dcp-client 4.1.19 → 4.1.20
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.
- package/dist/dcp-client-bundle.js +18 -18
- package/libexec/evaluator-node.js +27 -4
- package/libexec/sandbox/access-lists.js +2 -1
- package/libexec/sandbox/bootstrap.js +2 -1
- package/libexec/sandbox/bravojs-env.js +120 -58
- package/libexec/sandbox/bravojs-init.js +2 -1
- package/libexec/sandbox/calculate-capabilities.js +2 -3
- package/libexec/sandbox/event-loop-virtualization.js +2 -3
- package/libexec/sandbox/native-event-loop.js +2 -2
- package/libexec/sandbox/script-load-wrapper.js +15 -3
- package/libexec/sandbox/webgpu-worker-environment.js +2 -1
- package/libexec/sandbox/wrap-event-listeners.js +2 -2
- package/package.json +1 -1
- package/libexec/sandbox/primitive-timers.js +0 -99
|
@@ -4714,7 +4714,7 @@ eval("\n\nvar alphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqr
|
|
|
4714
4714
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
4715
4715
|
|
|
4716
4716
|
"use strict";
|
|
4717
|
-
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"default\", function() { return Modal; });\n/**\n * A Small Modal Class\n * @module Modal\n */\n/* globals Event dcpConfig */\nclass Modal {\n constructor (title, message, callback = false, exitHandler = false, {\n continueLabel = 'Continue',\n cancelLabel = 'Cancel',\n cancelVisible = true\n } = {}) {\n const modal = document.createElement('div')\n modal.className = 'dcp-modal-container-old day'\n modal.innerHTML = `\n <dialog class=\"dcp-modal-content\">\n <div class=\"dcp-modal-header\">\n <h2>${title}<button type=\"button\" class=\"close\">×</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\":\"e648af2afccc9056e33cbb61eeb326a283dff158\",\"branch\":\"release\",\"dcpClient\":{\"version\":\"4.1.19\",\"from\":\"git+ssh://git@gitlab.com/Distributed-Compute-Protocol/dcp-client.git#release\",\"resolved\":\"git+ssh://git@gitlab.com/Distributed-Compute-Protocol/dcp-client.git#42bc3fd53e2937a47fc27b558a5bbf7b16cd443b\"},\"built\":\"Tue Feb 01 2022 13:56:55 GMT-0500 (Eastern Standard Time)\",\"config\":{\"generated\":\"Tue 01 Feb 2022 01:56:52 PM EST by erose on lorge\",\"build\":\"debug\"},\"webpack\":\"4.46.0\",\"node\":\"v12.22.9\"} !== 'undefined' && typeof window.Modal === 'undefined') {\n window.Modal = Modal\n }\n}\n\n\n//# sourceURL=webpack:///./portal/www/js/modal.js?");
|
|
4717
|
+
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"default\", function() { return Modal; });\n/**\n * A Small Modal Class\n * @module Modal\n */\n/* globals Event dcpConfig */\nclass Modal {\n constructor (title, message, callback = false, exitHandler = false, {\n continueLabel = 'Continue',\n cancelLabel = 'Cancel',\n cancelVisible = true\n } = {}) {\n const modal = document.createElement('div')\n modal.className = 'dcp-modal-container-old day'\n modal.innerHTML = `\n <dialog class=\"dcp-modal-content\">\n <div class=\"dcp-modal-header\">\n <h2>${title}<button type=\"button\" class=\"close\">×</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\":\"afa05a15457fc3feb611804dc4f2159bba2ff452\",\"branch\":\"release\",\"dcpClient\":{\"version\":\"4.1.19\",\"from\":\"git+ssh://git@gitlab.com/Distributed-Compute-Protocol/dcp-client.git#release\",\"resolved\":\"git+ssh://git@gitlab.com/Distributed-Compute-Protocol/dcp-client.git#bad4562e53079f9ed48400c7d25be63d429ba36c\"},\"built\":\"Tue Feb 08 2022 11:12:59 GMT-0500 (Eastern Standard Time)\",\"config\":{\"generated\":\"Tue 08 Feb 2022 11:12:57 AM EST by erose on lorge\",\"build\":\"debug\"},\"webpack\":\"4.46.0\",\"node\":\"v12.22.10\"} !== 'undefined' && typeof window.Modal === 'undefined') {\n window.Modal = Modal\n }\n}\n\n\n//# sourceURL=webpack:///./portal/www/js/modal.js?");
|
|
4718
4718
|
|
|
4719
4719
|
/***/ }),
|
|
4720
4720
|
|
|
@@ -4737,7 +4737,7 @@ eval("/** @file Provide a standard set of DCP CLI options and related util
|
|
|
4737
4737
|
/***/ (function(module, exports, __webpack_require__) {
|
|
4738
4738
|
|
|
4739
4739
|
"use strict";
|
|
4740
|
-
eval("/* WEBPACK VAR INJECTION */(function(setImmediate) {/**\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 }
|
|
4740
|
+
eval("/* WEBPACK VAR INJECTION */(function(setImmediate) {/**\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\");\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 assert(typeof old !== 'undefined',\n typeof neu !== 'undefined',\n old !== neu);\n \n if (this._ !== old)\n {\n let lastCaller = this.lastStack.split('\\n')[2].replace(/^ *at /, '');\n throw new DCPError(`Cannot transition synchronizer from ${old}->${neu}; was in ${this._} from ${lastCaller}`, 'DCP_SYNCHRONIZER_ESYNC');\n }\n\n this._ = neu;\n this.lastStack = new Error().stack;\n assertHas(this.assertValues, this._);\n this.emit('change', this._, old);\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 ceci = this;\n \n function pWrapper(resolve, reject)\n {\n if (ceci._ === which)\n {\n setImmediate(resolve);\n return;\n }\n\n function changeHandlerOne(newState)\n {\n if (newState !== which || ceci._ !== which)\n return;\n\n ceci.removeEventListener('change', changeHandlerOne);\n ceci.removeEventListener('destroy', destroyHandler);\n resolve();\n }\n\n function changeHandlerMany(newState)\n {\n if (which.indexOf(newState) === -1 && which.indexOf(ceci._) === -1)\n return;\n\n ceci.removeEventListener('change', changeHandlerMany);\n ceci.removeEventListener('destroy', destroyHandler);\n resolve();\n }\n\n function destroyHandler()\n {\n ceci.removeEventListener(changeHandlerMany);\n ceci.removeEventListener(changeHandlerOne);\n ceci.removeEventListener(destroyHandler);\n reject();\n }\n\n ceci.on('change', Array.isArray(which) ? changeHandlerMany : changeHandlerOne);\n ceci.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 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 return test === this._;\n}\nexports.Synchronizer.prototype.is = exports.Synchronizer.prototype.test;\n\nexports.Synchronizer.prototype.isNot = function concurrency$$Synchronizer$isNot(test)\n{\n return test !== this._;\n}\n\nexports.Synchronizer.prototype.in = function concurrency$$Synchronizer$in(list) {\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/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(/*! ./../../node_modules/timers-browserify/main.js */ \"./node_modules/timers-browserify/main.js\").setImmediate))\n\n//# sourceURL=webpack:///./src/common/concurrency.js?");
|
|
4741
4741
|
|
|
4742
4742
|
/***/ }),
|
|
4743
4743
|
|
|
@@ -4881,7 +4881,7 @@ eval("/**\n * @file dcp-localstorage.js\n * @author Ryan Rossiter, r
|
|
|
4881
4881
|
/*! no static exports found */
|
|
4882
4882
|
/***/ (function(module, exports, __webpack_require__) {
|
|
4883
4883
|
|
|
4884
|
-
eval("/**\n * @file job/publish.js - Library code used by clients and tools (eg dcp-publish) to publish packages.\n * Orginally part of dcpDeployPakage.\n *\n * @author Ryan Rossiter, ryan@kingsds.network\n * Greg Agnew, gagnew@sparc.network\n * @date July 2020\n */\n\nconst { Connection } = __webpack_require__(/*! dcp/protocol-v4 */ \"./src/protocol-v4/index.js\");\nconst { getId } = __webpack_require__(/*! dcp/dcp-client/wallet */ \"./src/dcp-client/wallet/index.js\");\nconst { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\nconst debugging = __webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope('dcp-publish');\nconst { DCPError } = __webpack_require__(/*! ./dcp-error */ \"./src/common/dcp-error.js\");\nconst { DcpURL } = __webpack_require__(/*! dcp/common/dcp-url */ \"./src/common/dcp-url.js\");\n\nconst log = (...args) => {\n if (debugging()) {\n console.debug('publish', ...args);\n }\n};\n\n/**\n * @typedef {object} PackageDescriptor\n * @property {string} name\n * @property {string} version\n * @property {Object.<string, string>} files Maps source file path to destination file name.\n *//**\n * @typedef {object} DeployPackagePayload\n * @property {string} name\n * @property {string} version\n * @property {Object.<string, string>} files Maps destination filename to file contents.\n */\n\n/**\n * @param {PackageDescriptor} pkg\n * @param {string} baseDir Directory to use as the base when resolving paths to files.\n * @returns {DeployPackagePayload}\n */\nfunction buildDeployPackagePayload(pkg, baseDir) {\n const { name, version, files } = pkg;\n const fs = requireNative('fs');\n const path = requireNative('path');\n\n if (!Object.keys(files).length) {\n throw new Error('Cannot deploy a package with no files');\n }\n\n return {\n name, version,\n files: Object.entries(files).reduce((files, [sourcePath, destName]) => {\n const filepath = path.resolve(baseDir, sourcePath);\n files[destName] = fs.readFileSync(filepath, { encoding: 'utf-8' });\n return files;\n }, {}),\n };\n}\n\n/** @typedef {import('dcp/dcp-client/wallet/keystore').Keystore} Keystore */\n\n/**\n * Publish a DCP package to the package manager identified by the current configuration.\n *\n * @param {PackageDescriptor | string} pkg An object describing the package or a filepath string.\n * @param {Keystore} [id] ID Keystore to deploy the package with, defaults to wallet.getId.\n * \n * @throws when the package can't be located or is is rejected by the package manager. \n */\nexports.publish = async function publish(pkg, id) {\n const fs = requireNative('fs');\n const path = requireNative('path');\n\n let baseDir = '.';\n if (typeof pkg === 'string') {\n /**\n * set baseDir to resolve files listed in the package descriptor file\n * relative to that file.\n */\n baseDir = path.dirname(pkg);\n pkg = JSON.parse(fs.readFileSync(pkg));\n }\n \n if (!id)\n {\n id = await getId();\n }\n\n const deployPackagePayload = buildDeployPackagePayload(pkg, baseDir);\n log('deployPackage Request Payload:', deployPackagePayload);\n\n let packageManagerConnection;\n try\n {\n const packageManagerTarget = { location: new DcpURL(dcpConfig.packageManager.location) };\n packageManagerConnection = new Connection(\n packageManagerTarget,\n id,\n );\n\n const {\n success,\n payload: deployPackageResponsePayload,\n } = await packageManagerConnection.send(\n 'deployPackage',\n deployPackagePayload,\n );\n log('deployPackage Response Payload:', deployPackageResponsePayload);\n\n if (!success) {\n throw new DCPError(\n 'Unable to publish DCP package',\n deployPackageResponsePayload,\n );\n }\n\n return deployPackageResponsePayload;\n }\n finally\n {\n /**\n * If instantiation of the connection fails, prevent throwing an obscure\n * \"close of undefined\" error.\n */\n if (packageManagerConnection)\n {\n await packageManagerConnection.close();\n }\n }\n};\n\n\n//# sourceURL=webpack:///./src/common/dcp-publish.js?");
|
|
4884
|
+
eval("/**\n * @file job/publish.js - Library code used by clients and tools (eg dcp-publish) to publish packages.\n * Orginally part of dcpDeployPakage.\n *\n * @author Ryan Rossiter, ryan@kingsds.network\n * Greg Agnew, gagnew@sparc.network\n * @date July 2020\n */\n\nconst { Connection } = __webpack_require__(/*! dcp/protocol-v4 */ \"./src/protocol-v4/index.js\");\nconst { getId } = __webpack_require__(/*! dcp/dcp-client/wallet */ \"./src/dcp-client/wallet/index.js\");\nconst { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\nconst debugging = __webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope('dcp-publish');\nconst { DCPError } = __webpack_require__(/*! ./dcp-error */ \"./src/common/dcp-error.js\");\nconst { DcpURL } = __webpack_require__(/*! dcp/common/dcp-url */ \"./src/common/dcp-url.js\");\n\nconst log = (...args) => {\n if (debugging()) {\n console.debug('publish', ...args);\n }\n};\n\n/**\n * @typedef {object} PackageDescriptor\n * @property {string} name\n * @property {string} version\n * @property {Object.<string, string>} files Maps source file path to destination file name.\n *//**\n * @typedef {object} DeployPackagePayload\n * @property {string} name\n * @property {string} version\n * @property {Object.<string, string>} files Maps destination filename to file contents.\n */\n\n/**\n * @param {PackageDescriptor} pkg\n * @param {string} baseDir Directory to use as the base when resolving paths to files.\n * @returns {DeployPackagePayload}\n */\nfunction buildDeployPackagePayload(pkg, baseDir) {\n const { name, version, files } = pkg;\n const fs = requireNative('fs');\n const path = requireNative('path');\n\n if (!Object.keys(files).length) {\n throw new Error('Cannot deploy a package with no files');\n }\n\n return {\n name, version,\n files: Object.entries(files).reduce((files, [sourcePath, destName]) => {\n const filepath = path.resolve(baseDir, sourcePath);\n files[destName] = fs.readFileSync(filepath, { encoding: 'utf-8' });\n return files;\n }, {}),\n };\n}\n\n/** @typedef {import('dcp/dcp-client/wallet/keystore').Keystore} Keystore */\n\n/**\n * Publish a DCP package to the package manager identified by the current configuration.\n *\n * @param {PackageDescriptor | string} pkg An object describing the package or a filepath string.\n * @param {Keystore} [id] ID Keystore to deploy the package with, defaults to wallet.getId.\n * \n * @throws when the package can't be located or is is rejected by the package manager. \n */\nexports.publish = async function publish(pkg, id) {\n const fs = requireNative('fs');\n const path = requireNative('path');\n\n let baseDir = '.';\n if (typeof pkg === 'string') {\n /**\n * set baseDir to resolve files listed in the package descriptor file\n * relative to that file.\n */\n baseDir = path.dirname(pkg);\n pkg = JSON.parse(fs.readFileSync(pkg));\n }\n \n if (!id)\n {\n id = await getId();\n }\n\n const deployPackagePayload = buildDeployPackagePayload(pkg, baseDir);\n log('deployPackage Request Payload:', deployPackagePayload);\n\n let packageManagerConnection;\n try\n {\n const packageManagerTarget = { location: new DcpURL(dcpConfig.packageManager.location) };\n packageManagerConnection = new Connection(\n packageManagerTarget,\n id,\n );\n\n const {\n success,\n payload: deployPackageResponsePayload,\n } = await packageManagerConnection.send(\n 'deployPackage',\n deployPackagePayload,\n );\n log('deployPackage Response Payload:', deployPackageResponsePayload);\n\n if (!success) {\n throw new DCPError(\n 'Unable to publish DCP package',\n deployPackageResponsePayload,\n );\n }\n\n return deployPackageResponsePayload;\n }\n catch(error)\n {\n console.error('Unable to publish DCP package:', error);\n throw error;\n }\n finally\n {\n /**\n * If instantiation of the connection fails, prevent throwing an obscure\n * \"close of undefined\" error.\n */\n if (packageManagerConnection)\n {\n await packageManagerConnection.close();\n }\n }\n};\n\n\n//# sourceURL=webpack:///./src/common/dcp-publish.js?");
|
|
4885
4885
|
|
|
4886
4886
|
/***/ }),
|
|
4887
4887
|
|
|
@@ -4892,7 +4892,7 @@ eval("/**\n * @file job/publish.js - Library code used by clients and too
|
|
|
4892
4892
|
/*! no static exports found */
|
|
4893
4893
|
/***/ (function(module, exports, __webpack_require__) {
|
|
4894
4894
|
|
|
4895
|
-
eval("/* WEBPACK VAR INJECTION */(function(process, setImmediate) {/**\n * @file dcp-timer.js\n * Simple timer which behaves very much like setInterval\n * @author Wes Garland, wes@kingsds.network\n * @date Dec 2019\n */\n\nconst debugging = new __webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope('timers', exports);\nlet timerSeq = 0;\n\n/**\n * setImmediate, but for a specific number of event loop passes.\n * @param {function} callback The callback to run\n * @param {number} N The number of event loop passes to wait before running callback\n */\nexports.setImmediateN = function setImmediateN(callback, N)\n{\n switch(N)\n {\n case 0:\n process.nextTick(callback);\n break;\n case 1:\n setImmediate(callback);\n break;\n default:\n N = Math.max(N-1 || 0, 0);\n setImmediate(() => setImmediateN(callback, N));\n break;\n }\n}\n\n/**\n * Exponential backoff function similar to IEEE 802.3 CSMA/CD standard in that it\n * chooses randomly from an exponentially growing pool of values. Also supports truncating\n * at a max value and starting the range at slot instead of 0.\n * \n * This system will never generate a number greater than max. That means if max falls between\n * two ranges, the smaller will be chosen. For example if the first range is [0,4] and the next is [0,8]\n * and the max is 7, then numbers will be drawn from [0,4] and never grow to the [0,8] range.\n * \n * @param {number} slot All values returned by this will be an integer multiple of the slot value.\n * @param {number} [max] Truncate exponential growth at this value if provided.\n * @param {number} [base=2] Exponential growth factor (must be greater than 1 for truncation to work).\n * @param {boolean} [includeZero=true] Usually 0 is a possible value. If we exclude it then we always \n * return a value of at least `slot`.\n * @example - slot is 5, no other defaults used.\n * const backoff = makeEBOIterator(5);\n * backoff.next().value // chosen randomly from: [0, 5]\n * backoff.next().value // chosen randomly from: [0, 5, 10, 15] = [0, slot * 3]\n * backoff.next().value // chosen randomly from: [0, 5, ... 30, 35] = [0, slot * 7] = [0, slot * 2^iteration-1]\n */\nexports.makeEBOIterator = function* dcpTimer$$makeEBOIterator(slot, max=null, base=2) {\n let truncated = false;\n let iteration = 1;\n let maxSlots, k, value, maxVal;\n\n while (true) {\n if (!truncated) {\n maxSlots = Math.pow(base, iteration);\n maxVal = maxSlots * slot;\n truncated = max && maxVal >= max;\n // if we overshot the max\n if (maxVal > max) maxSlots = Math.pow(base, --iteration);\n if (!truncated) iteration++;\n }\n\n k = Math.floor(Math.random() * maxSlots);\n value = slot * k;\n yield value;\n }\n}\n\n/** Exponential backoff timer. Works like setInterval, except it has an exponential \n * delay, described by the timerConfig object.\n *\n * @param fun {object} The function to call every interval\n * @param timerConfig {object} An object describing the backoff rate:\n * .maxInterval - the longest possible interval to wait\n * .baseInterval - the shortest possible interval to wait\n * .backoffFactor - the rate at which we move from base to max. \n * @returns a timer id suitable for use with clearBackoffInterval\n */\nexports.setBackoffInterval = function dcpTimer$$setBackoffInterval(\n fun,\n timerConfig,\n) {\n if (typeof timerConfig === 'undefined' || timerConfig === null) {\n throw new TypeError(\n `Timer configuration (${timerConfig}) cannot be undefined or null.`,\n );\n }\n\n const timerHnd = { id: ++timerSeq };\n let n = 0;\n let isUnref = false;\n\n debugging('backoff') && console.log('timers - creating backoff timer', timerHnd.id, timerConfig.label || '');\n\n const eboIterator = exports.makeEBOIterator(timerConfig.baseInterval, timerConfig.maxInterval, timerConfig.backoffFactor);\n\n function backoffIntervalWrapper() {\n if (timerHnd.currentTimeoutId === 'cancel')\n return;\n const nextInterval = eboIterator.next().value;\n debugging('backoff') && console.log(`timers - servicing backoff timer ${timerHnd.id} with ${fun.name || 'unnamed callback function'}; nextInterval is ${Math.floor(nextInterval)}ms`);\n fun();\n if (timerHnd.currentTimeoutId !== 'cancel') {\n timerHnd.currentTimeoutId = setTimeout(backoffIntervalWrapper, nextInterval);\n if (isUnref)\n timerHnd.unref();\n }\n };\n \n setImmediate(backoffIntervalWrapper).unref;\n timerHnd.unref = function() { \n isUnref = true;\n if (timerHnd.currentTimeoutId)\n timerHnd.currentTimeoutId.unref();\n };\n return timerHnd;\n}\n\n/** Clear a backoff interval - prevent the timer from running. \n * @param timerHnd A timer handle returned from setBackoffInterval\n */\nexports.clearBackoffInterval = function dcpTimer$$clearBackoffInterval(timerHnd) {\n clearTimeout(timerHnd.currentTimeoutId);\n timerHnd.currentTimeoutId = 'cancel';\n}\n\n/**\n * Registry that contains information about the current timeouts from\n * dcpSetTimeout. Each object is removed from the array after the\n * timeout has completed.\n *\n * This takes the form of an array of objects where each object has\n * four properties:\n * delay MS before callback is executed\n * initialTime MS Unix time dcpSetInterval was called\n * initalTimePlusDelay MS Unix time callback can be executed\n * callback The callback function to be execuetd\n */\nexports.timeoutRegistry = [];\n\n/**\n * Wrapper for setTimeout that contains the entire error stack when\n * an error is thrown in the callback and tracks timeouts in a registry.\n *\n * @param callback Function that executes after the delay\n * @param delay Delay in milliseconds\n * @param ...args Parameters that are passed into the callback\n */\nexports.dcpSetTimeout = function dcpTimer$$dcpSetTimeout(callback, delay, ...args) {\n // add an entry for this timeout into the registry (all in MS)\n let registry = exports.timeoutRegistry ;\n let currentDate = Date.now();\n const timeoutEntry = {\n delay,\n initialTime: Date(currentDate),\n initialTimePlusDelay: Date(currentDate += delay),\n callback,\n };\n registry.push(timeoutEntry);\n\n // capture the current stack and add it to the error if callback throws\n const outerStack = (new Error()).stack;\n\n setTimeout(() => {\n // remove from the registry\n registry.splice(registry.indexOf(timeoutEntry), 1);\n\n try {\n callback.apply(null, args);\n } catch (error) {\n error.stack = error.stack.concat(outerStack);\n throw error;\n }\n }, delay);\n}\n\n\n/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(/*! ./../../node_modules/process/browser.js */ \"./node_modules/process/browser.js\"), __webpack_require__(/*! ./../../node_modules/timers-browserify/main.js */ \"./node_modules/timers-browserify/main.js\").setImmediate))\n\n//# sourceURL=webpack:///./src/common/dcp-timers.js?");
|
|
4895
|
+
eval("/* WEBPACK VAR INJECTION */(function(process, setImmediate) {/**\n * @file dcp-timer.js\n * Simple timer which behaves very much like setInterval\n * @author Wes Garland, wes@kingsds.network\n * @date Dec 2019\n */\n\nconst debugging = new __webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope('timers', exports);\nlet timerSeq = 0;\n\n/**\n * setImmediate, but for a specific number of event loop passes.\n * @param {function} callback The callback to run\n * @param {number} N The number of event loop passes to wait before running callback\n */\nexports.setImmediateN = function setImmediateN(callback, N)\n{\n switch(N)\n {\n case 0:\n process.nextTick(callback);\n break;\n case 1:\n setImmediate(callback);\n break;\n default:\n N = Math.max(N-1 || 0, 0);\n setImmediate(() => setImmediateN(callback, N));\n break;\n }\n}\n\n/**\n * Exponential backoff function similar to IEEE 802.3 CSMA/CD standard in that it\n * chooses randomly from an exponentially growing pool of values. Also supports truncating\n * at a max value and starting the range at slot instead of 0.\n * \n * This system will never generate a number greater than max. That means if max falls between\n * two ranges, the smaller will be chosen. For example if the first range is [0,4] and the next is [0,8]\n * and the max is 7, then numbers will be drawn from [0,4] and never grow to the [0,8] range.\n * \n * @param {number} slot All values returned by this will be an integer multiple of the slot value.\n * @param {number} [max] Truncate exponential growth at this value if provided.\n * @param {number} [base=2] Exponential growth factor (must be greater than 1 for truncation to work).\n * @param {boolean} [includeZero=true] Usually 0 is a possible value. If we exclude it then we always \n * return a value of at least `slot`.\n * @example - slot is 5, no other defaults used.\n * const backoff = makeEBOIterator(5);\n * backoff.next().value // chosen randomly from: [0, 5]\n * backoff.next().value // chosen randomly from: [0, 5, 10, 15] = [0, slot * 3]\n * backoff.next().value // chosen randomly from: [0, 5, ... 30, 35] = [0, slot * 7] = [0, slot * 2^iteration-1]\n */\nexports.makeEBOIterator = function* dcpTimer$$makeEBOIterator(slot, max=null, base=2) {\n let truncated = false;\n let iteration = 1;\n let maxSlots, k, value, maxVal;\n\n while (true) {\n if (!truncated) {\n maxSlots = Math.pow(base, iteration);\n maxVal = maxSlots * slot;\n truncated = max && maxVal >= max;\n // if we overshot the max\n if (maxVal > max) maxSlots = Math.pow(base, --iteration);\n if (!truncated) iteration++;\n }\n\n k = Math.floor(Math.random() * maxSlots);\n value = slot * k;\n yield value;\n }\n}\n\n/** Exponential backoff timer. Works like setInterval, except it has an exponential \n * delay, described by the timerConfig object.\n *\n * @param fun {object} The function to call every interval\n * @param timerConfig {object} An object describing the backoff rate:\n * .maxInterval - the longest possible interval to wait\n * .baseInterval - the shortest possible interval to wait\n * .backoffFactor - the rate at which we move from base to max. \n * @returns a timer id suitable for use with clearBackoffInterval\n */\nexports.setBackoffInterval = function dcpTimer$$setBackoffInterval(\n fun,\n timerConfig,\n) {\n if (typeof timerConfig === 'undefined' || timerConfig === null) {\n throw new TypeError(\n `Timer configuration (${timerConfig}) cannot be undefined or null.`,\n );\n }\n\n const timerHnd = { id: ++timerSeq };\n let n = 0;\n let isUnref = false;\n\n debugging('backoff') && console.log('timers - creating backoff timer', timerHnd.id, timerConfig.label || '');\n\n const eboIterator = exports.makeEBOIterator(timerConfig.baseInterval, timerConfig.maxInterval, timerConfig.backoffFactor);\n\n function backoffIntervalWrapper() {\n if (timerHnd.currentTimeoutId === 'cancel')\n return;\n const nextInterval = eboIterator.next().value;\n debugging('backoff') && console.log(`timers - servicing backoff timer ${timerHnd.id} with ${fun.name || 'unnamed callback function'}; nextInterval is ${Math.floor(nextInterval)}ms`);\n fun(nextInterval);\n if (timerHnd.currentTimeoutId !== 'cancel') {\n timerHnd.currentTimeoutId = setTimeout(backoffIntervalWrapper, nextInterval);\n if (isUnref)\n timerHnd.unref();\n }\n };\n \n setImmediate(backoffIntervalWrapper).unref;\n timerHnd.unref = function() { \n isUnref = true;\n if (timerHnd.currentTimeoutId)\n timerHnd.currentTimeoutId.unref();\n };\n return timerHnd;\n}\n\n/** Clear a backoff interval - prevent the timer from running. \n * @param timerHnd A timer handle returned from setBackoffInterval\n */\nexports.clearBackoffInterval = function dcpTimer$$clearBackoffInterval(timerHnd) {\n clearTimeout(timerHnd.currentTimeoutId);\n timerHnd.currentTimeoutId = 'cancel';\n}\n\n/**\n * Registry that contains information about the current timeouts from\n * dcpSetTimeout. Each object is removed from the array after the\n * timeout has completed.\n *\n * This takes the form of an array of objects where each object has\n * four properties:\n * delay MS before callback is executed\n * initialTime MS Unix time dcpSetInterval was called\n * initalTimePlusDelay MS Unix time callback can be executed\n * callback The callback function to be execuetd\n */\nexports.timeoutRegistry = [];\n\n/**\n * Wrapper for setTimeout that contains the entire error stack when\n * an error is thrown in the callback and tracks timeouts in a registry.\n *\n * @param callback Function that executes after the delay\n * @param delay Delay in milliseconds\n * @param ...args Parameters that are passed into the callback\n */\nexports.dcpSetTimeout = function dcpTimer$$dcpSetTimeout(callback, delay, ...args) {\n // add an entry for this timeout into the registry (all in MS)\n let registry = exports.timeoutRegistry ;\n let currentDate = Date.now();\n const timeoutEntry = {\n delay,\n initialTime: Date(currentDate),\n initialTimePlusDelay: Date(currentDate += delay),\n callback,\n };\n registry.push(timeoutEntry);\n\n // capture the current stack and add it to the error if callback throws\n const outerStack = (new Error()).stack;\n\n setTimeout(() => {\n // remove from the registry\n registry.splice(registry.indexOf(timeoutEntry), 1);\n\n try {\n callback.apply(null, args);\n } catch (error) {\n error.stack = error.stack.concat(outerStack);\n throw error;\n }\n }, delay);\n}\n\n\n/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(/*! ./../../node_modules/process/browser.js */ \"./node_modules/process/browser.js\"), __webpack_require__(/*! ./../../node_modules/timers-browserify/main.js */ \"./node_modules/timers-browserify/main.js\").setImmediate))\n\n//# sourceURL=webpack:///./src/common/dcp-timers.js?");
|
|
4896
4896
|
|
|
4897
4897
|
/***/ }),
|
|
4898
4898
|
|
|
@@ -4938,7 +4938,7 @@ eval("/**\n * @file hash.js\n * General purpose utility rout
|
|
|
4938
4938
|
/***/ (function(module, exports, __webpack_require__) {
|
|
4939
4939
|
|
|
4940
4940
|
"use strict";
|
|
4941
|
-
eval("/* WEBPACK VAR INJECTION */(function(global) {/**\n * @file scheduler-constants.js\n * Contants and constant-like precomputed values for use with/by DCPv4.\n * All values in this module are completely deterministic and will change\n * only if the source code changes.\n * @author Wes Garland, wes@kingsds.network\n * @date Nov 2020\n */\n\n\n/** Pre-defined, hard-coded compute groups */\nexports.computeGroups = {\n public: {\n opaqueId: 'WHhetL7mj1w1mw1XV6dxyC', \n id: 1,\n name: 'Public Compute Group (open access)',\n joinKey: 'public',\n joinSecret: '',\n joinKeystore : false /* string / object/ falsey */\n },\n};\n\n/**\n * The list of all possible job status in the status column of the jobs table.\n */\nexports.jobStatus = new SchedulerConstantGroup(\n 'cancelled',\n 'corrupted',\n 'estimation',\n 'finished',\n 'running',\n);\n\n/**\n * The list of all possible slice status in the status column of the\n * activeSlices table.\n */\nexports.sliceStatus = new SchedulerConstantGroup(\n 'overdue',\n 'tiebreaker',\n 'scheduled',\n 'working',\n 'paused',\n 'returned',\n 'new',\n);\n\n/** Currently used bit masks for flags column of jobs table. There is capacity for 31 bit masks. */\nexports.jobFlags = {\n localExec: 1 << 0, /* local exec job; prevent from joining compute groups */\n open: 1 << 1, /* job still open, i.e. able to add more slices */\n force100pctCPUDensity: 1 << 29, /* Temporary flag that considers the wall clock vs cpu time */\n};\n\nfunction SchedulerConstantGroup()\n{\n var argv = Array.from(arguments);\n var dcpConfig = global.hasOwnProperty('dcpConfig') ? global.dcpConfig : {};\n\n for (let el of argv)\n this[el] = el;\n\n if (dcpConfig.build === 'debug')\n {\n let px = new Proxy(this, {\n get: (target, prop) => {\n if (!target.hasOwnProperty(prop))\n throw new Error(`no such constant: '${prop}'`);\n return target[prop];\n },\n set: (target, prop) => {\n throw new Error('constants are immutable!');\n },\n delete: (prop) => {\n throw new Error('constant groups are immutable!');\n },\n });\n\n return px;\n }\n}\n\nexports.workerIdLength = 22;\n\n\n/** Temporary arrays for backwards compatibility - do not use for new code */\nexports.sliceStatuses = Object.keys(exports.sliceStatus);\nexports.jobStatuses = Object.keys(exports.jobStatus);\n\n/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(/*! ./../../node_modules/webpack/buildin/global.js */ \"./node_modules/webpack/buildin/global.js\")))\n\n//# sourceURL=webpack:///./src/common/scheduler-constants.js?");
|
|
4941
|
+
eval("/* WEBPACK VAR INJECTION */(function(global) {/**\n * @file scheduler-constants.js\n * Contants and constant-like precomputed values for use with/by DCPv4.\n * All values in this module are completely deterministic and will change\n * only if the source code changes.\n * @author Wes Garland, wes@kingsds.network\n * @date Nov 2020\n */\n\n\n/** Pre-defined, hard-coded compute groups */\nexports.computeGroups = {\n public: {\n opaqueId: 'WHhetL7mj1w1mw1XV6dxyC', \n id: 1,\n name: 'Public Compute Group (open access)',\n joinKey: 'public',\n joinSecret: '',\n joinKeystore : false /* string / object/ falsey */\n },\n};\n\n/**\n * The list of all possible job status in the status column of the jobs table.\n */\nexports.jobStatus = new SchedulerConstantGroup(\n 'cancelled',\n 'corrupted',\n 'estimation',\n 'finished',\n 'running',\n);\n\n/**\n * The list of all possible slice status in the status column of the\n * activeSlices table.\n */\nexports.sliceStatus = new SchedulerConstantGroup(\n 'overdue',\n 'tiebreaker',\n 'scheduled',\n 'working',\n 'paused',\n 'returned',\n 'new',\n);\n\n/** Currently used bit masks for flags column of jobs table. There is capacity for 31 bit masks. */\nexports.jobFlags = {\n localExec: 1 << 0, /* local exec job; prevent from joining compute groups */\n open: 1 << 1, /* job still open, i.e. able to add more slices */\n workerConsole: 1 << 2, /* job is allowed to log to worker's console - if worker permits also */\n force100pctCPUDensity: 1 << 29, /* Temporary flag that considers the wall clock vs cpu time */\n};\n\nfunction SchedulerConstantGroup()\n{\n var argv = Array.from(arguments);\n var dcpConfig = global.hasOwnProperty('dcpConfig') ? global.dcpConfig : {};\n\n for (let el of argv)\n this[el] = el;\n\n if (dcpConfig.build === 'debug')\n {\n let px = new Proxy(this, {\n get: (target, prop) => {\n if (!target.hasOwnProperty(prop))\n throw new Error(`no such constant: '${prop}'`);\n return target[prop];\n },\n set: (target, prop) => {\n throw new Error('constants are immutable!');\n },\n delete: (prop) => {\n throw new Error('constant groups are immutable!');\n },\n });\n\n return px;\n }\n}\n\nexports.workerIdLength = 22;\n\n\n/** Temporary arrays for backwards compatibility - do not use for new code */\nexports.sliceStatuses = Object.keys(exports.sliceStatus);\nexports.jobStatuses = Object.keys(exports.jobStatus);\n\n/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(/*! ./../../node_modules/webpack/buildin/global.js */ \"./node_modules/webpack/buildin/global.js\")))\n\n//# sourceURL=webpack:///./src/common/scheduler-constants.js?");
|
|
4942
4942
|
|
|
4943
4943
|
/***/ }),
|
|
4944
4944
|
|
|
@@ -5037,7 +5037,7 @@ eval("/**\n * @file password.js\n * Modal providing a way to
|
|
|
5037
5037
|
/*! no static exports found */
|
|
5038
5038
|
/***/ (function(module, exports, __webpack_require__) {
|
|
5039
5039
|
|
|
5040
|
-
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=
|
|
5040
|
+
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=afa05a15457fc3feb611804dc4f2159bba2ff452'; /* installer token */\n return '?ucp=' + Date.now();\n}\n \n/* Detect load type - on webpack, load dynamic content relative to webpack bundle;\n * otherwise load relative to the current scheduler's configured portal.\n */\nexports.myScript = (typeof document !== 'undefined') && document.currentScript;\nexports.corsProxyHref = undefined;\nif (exports.myScript && exports.myScript === __webpack_require__(/*! ./fetch-relative */ \"./src/dcp-client/client-modal/fetch-relative.js\").myScript) {\n let url = new (__webpack_require__(/*! dcp/common/dcp-url */ \"./src/common/dcp-url.js\").DcpURL)(exports.myScript.src);\n exports.corsProxyHref = url.resolve('../cors-proxy.html');\n}\n\n/**\n * Look for modal id and required ids on page based on config, if not found, provide from dcp-client.\n * The first id in the required array must be the id of the modal's form element.\n * @param {Object} modalConfig Modal configuration object\n * @param {string} modalConfig.id Id of parent modal element\n * @param {string[]} modalConfig.required Array of required ids in parent modal element\n * @param {string[]} [modalConfig.optional] Array of optional ids in parent modal element\n * @param {string} modalConfig.path Relative path to modal html in dcp-client\n * @returns {DOMElement[]} Array of modal elements on page [config.id, ...config.required]\n */\nexports.initModal = async function (modalConfig, onClose) {\n exports.corsProxyHref = exports.corsProxyHref || dcpConfig.portal.location.resolve('dcp-client/cors-proxy.html');\n\n // Call ensure modal on any eager-loaded modals.\n if (modalConfig.eagerLoad) {\n Promise.all(\n modalConfig.eagerLoad.map(config => ensureModal(config))\n )\n };\n\n const [elements, optionalElements] = await ensureModal(modalConfig);\n\n // Wire up form to prevent default, resolve on submission, reject+reset when closed (or call onClose when closed)\n const [modal, form] = elements;\n form.reset(); // ensure that form is fresh\n let formResolve, formReject;\n let formPromise = new Promise( function(res, rej) {\n formResolve = res;\n formReject = rej;\n });\n form.onsubmit = function (submitEvent) {\n submitEvent.preventDefault();\n modal.setAttribute(\"data-state\", \"submitted\");\n formResolve(submitEvent);\n }\n\n exports.MicroModal.show(modalConfig.id, { \n disableFocus: true, \n onClose: onClose || getDefaultOnClose(formReject)\n });\n return [elements, formPromise, optionalElements];\n};\n\n// Ensure all required modal elements are on page according to modalConfig\nasync function ensureModal(modalConfig) {\n let allRequiredIds = [modalConfig.id, ...modalConfig.required];\n let missing = allRequiredIds.filter( id => !document.getElementById(id) );\n if (missing.length > 0) {\n if (missing.length !== allRequiredIds.length)\n console.warn(`Some of the ids needed to replace the default DCP-modal were found, but not all. So the default DCP-Modal will be used. Missing ids are: [${missing}].`);\n let contents = await fetchRelative(exports.corsProxyHref, modalConfig.path + cachePoison());\n const container = document.createElement('div');\n container.innerHTML = contents;\n document.body.appendChild(container);\n }\n\n const elements = allRequiredIds.map(id => document.getElementById(id));\n const optionalElements = (modalConfig.optional || []).map(id => document.getElementById(id));\n return [elements, optionalElements];\n};\n\n// This onClose is called by MicroModal and thus has the modal passed to it.\nfunction getDefaultOnClose (formReject) {\n return (modal) => {\n modal.offsetLeft; // forces style recalc\n const origState = modal.dataset.state;\n // reset form including data-state\n modal.setAttribute(\"data-state\", \"new\");\n // reject if closed without submitting form.\n if (origState !== \"submitted\") {\n const err = new DCPError(\"Modal was closed but modal's form was not submitted.\", exports.OnCloseErrorCode);\n formReject(err);\n }\n }\n}\n\n\n//# sourceURL=webpack:///./src/dcp-client/client-modal/utils.js?");
|
|
5041
5041
|
|
|
5042
5042
|
/***/ }),
|
|
5043
5043
|
|
|
@@ -5059,7 +5059,7 @@ eval("/**\n * @file Client facing module that implements Compute Groups API\n
|
|
|
5059
5059
|
/*! no static exports found */
|
|
5060
5060
|
/***/ (function(module, exports, __webpack_require__) {
|
|
5061
5061
|
|
|
5062
|
-
eval("/**\n * @file Module that implements Compute API\n * @module dcp/compute\n * @access public\n */\n\n/* global dcpConfig */\n\n/**\n * @access private - Will change soon!\n * @typedef {object} JobRequirements\n * @property {object} environment\n * @property {boolean} environment.fdlibm - Workers express capability 'fdlibm' in order for client applications to have confidence that results will be bitwise identical across workers. This library is recommended, but not required, for implementaiton of Google chrome, Mozilla Firefox and the DCP standalone worker use fdlibm but Microsoft Edge and Apple Safari do not use it.\n * @property {boolean} environment.offscreenCanvas When present, this capability indicates that the worker environment has the OffscreenCanvas symbol defined.\n * @property {object} details\n * @property {object} details.offscreenCanvas\n * @property {boolean} details.offscreenCanvas.bigTexture4096 This capability indicates that the worker's WebGL MAX_TEXTURE_SIZE is at least 4096.\n * @property {boolean} details.offscreenCanvas.bigTexture8192 This capability indicates that the worker's WebGL MAX_TEXTURE_SIZE is at least 8192.\n * @property {boolean} details.offscreenCanvas.bigTexture16384 This capability indicates that the worker's WebGL MAX_TEXTURE_SIZE is at least 16384.\n * @property {boolean} details.offscreenCanvas.bigTexture32768 This capability indicates that the worker's WebGL MAX_TEXTURE_SIZE is at least 32768.\n * @property {object} engine\n * @property {boolean} engine.es7 This capability enforces that the worker is running an es7-compliant JavaScript engine.\n * @property {boolean} engine.spidermonkey The capability enforces that the worker will run in the SpiderMonkey JS engine.\n * @property {boolean} browser.chrome The capability to exclude Chrome browsers.\n */\n\n/**\n * This JSDoc 'namespace' is a convenient way to document some globals that are available only in\n * the sandbox environment provided to work functions.\n * @access public\n * @namespace sandboxEnv\n */\n\n/**\n * This function emits a progress event. Progress events should be emitted approximately once per second; a task which fails to emit a \n * progress event for a certain period of time will be cancelled by the supervisor. The argument to this function is interpreted to six \n * significant digits, and must increase for every call. *All work functions must emit at least one progress event* - this requirement \n * will be enforced by the estimator. * The period of time mentioned above will be at least 30 wall-clock seconds and at least 30 \n * benchmark-adjusted seconds\n * @access public\n * @method progress\n * @param {string|number|undefined} n a number between 0 and 1 (inclusive) that represents a best-guess \n * at the completed portion of the task as a ratio of completed work to total work. If the argument is a string ending in the `%` symbol, \n * it will be interpreted as a number in the usual mathematical sense. If the argument is `undefined`, the slice progress is noted but \n * the amount of progress is considered to be indeterminate.\n * @memberof module:dcp/compute~sandboxEnv\n * @returns {boolean} true\n */\n\n/**\n * Emit a `console` event to the `JobHandle` in the client. If the sandbox is running in an environment with a native console object, the native method may also be invoked.\n * \n * @member console\n * @property {function} log fire console event with 'log' as `level` (see {@link Job#event:console|Job#event:console.level}).\n * @property {function} debug fire console event with 'debug' as `level` (see {@link Job#event:console|Job#event:console.level}).\n * @property {function} info fire console event with 'info' as `level` (see {@link Job#event:console|Job#event:console.level}).\n * @property {function} warn fire console event with 'warn' as `level` (see {@link Job#event:console|Job#event:console.level}).\n * @property {function} error fire console event with 'error' as `level` (see {@link Job#event:console|Job#event:console.level}).\n * \n * **Note** some ES environments (Chrome, Firefox) implement C-style print formatting in this method. This is currently not supported.\n * @memberof module:dcp/compute~sandboxEnv\n * @emits Job#console\n * @access public\n */\n\n/**\n * @member work An object that contains the following properties\n * @property {function} emit (eventName, ...) - send arbitrary args with `eventName`. This event is received client-side with those args (see {@link Job#work}).\n * @property {object} job\n * @property {object} job.public Property set on job handle, see {@link Job#public}.\n * @memberof module:dcp/compute~sandboxEnv\n * @access public\n */\n\n/**\n * @member {OffscreenCanvas} OffscreenCanvas As defined in the {@link https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvas|HTML Standard}, provides a canvas which can be rendered off-screen. If this interface is not available in a given worker, the worker will not report capability \"offscreenCanvas\".\n * @memberof module:dcp/compute~sandboxEnv\n * @access public\n */\n\nconst protocolV4 = __webpack_require__(/*! dcp/protocol-v4 */ \"./src/protocol-v4/index.js\");\nconst { EventEmitter } = __webpack_require__(/*! dcp/common/dcp-events/event-emitter */ \"./src/common/dcp-events/event-emitter.js\");\nconst { RangeObject, rehydrateRange } = __webpack_require__(/*! dcp/dcp-client/range-object */ \"./src/dcp-client/range-object.js\");\nconst { RemoteDataSet } = __webpack_require__(/*! dcp/dcp-client/remote-data-set */ \"./src/dcp-client/remote-data-set.js\");\nconst { RemoteDataPattern } = __webpack_require__(/*! dcp/dcp-client/remote-data-pattern */ \"./src/dcp-client/remote-data-pattern.js\");\nconst { Job, ResultHandle } = __webpack_require__(/*! dcp/dcp-client/job */ \"./src/dcp-client/job/index.js\");\nconst wallet = __webpack_require__(/*! dcp/dcp-client/wallet */ \"./src/dcp-client/wallet/index.js\");\nconst { DcpURL } = __webpack_require__(/*! dcp/common/dcp-url */ \"./src/common/dcp-url.js\");\nconst { disableWorker } = __webpack_require__(/*! dcp/dcp-client/worker */ \"./src/dcp-client/worker/index.js\");\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\nconst debugging = __webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope('dcp-client');\n\nconst debug = (...args) => {\n if (debugging()) {\n args.unshift('dcp-client:compute');\n console.debug(...args);\n }\n};\n\nconst MARKET_VALUE = Symbol('Market Value');\nconst indirectEval = eval; // eslint-disable-line no-eval\n\n/**\n * Instance methods of this class are exported as static members of {@link module:dcp/compute|dcp/compute}.\n * @example\n * let job = compute.for(1, 3, function (i) {\n * progress('100%');\n * return i*10;\n * })\n * let results = await job.exec();\n */\nclass Compute extends EventEmitter {\n constructor () {\n super('Compute');\n\n /** @todo move these into some central config */\n this.defaultMaxGPUs = 1;\n this.GPUsAssigned = 0;\n this.allowSlicesToFinishBeforeStopping = false;\n\n this.schedulerURL = undefined; // Will default to dcpConfig.scheduler.location if not overidden\n this.bankURL = undefined; // Will default to dcpConfig.bank.services.bankTeller.location if not overidden\n\n Object.defineProperty(this.marketValue, MARKET_VALUE, {\n value: true,\n writable: false,\n configurable: false,\n enumerable: false,\n });\n\n const ceci = this;\n this.RemoteDataSet = RemoteDataSet;\n this.RemoteDataPattern = RemoteDataPattern;\n // Initialize to null so these properties are recognized for the Job class\n this.phemeConnection = null;\n this.deployConnection = null;\n this.openPhemeConn = function openPhemeConn()\n {\n ceci.phemeConnection = new protocolV4.Connection(dcpConfig.scheduler.services.pheme);\n ceci.phemeConnection.on('close', ceci.openPhemeConn);\n }\n\n this.openDeployConn = function openDeployConn()\n {\n ceci.deployConnection = new protocolV4.Connection(dcpConfig.scheduler.services.jobSubmit);\n ceci.deployConnection.on('close', ceci.openDeployConn);\n }\n }\n\n get defaultMaxWorkers () {\n return portalWorker.supervisor.maxWorkingSandboxes\n }\n\n set defaultMaxWorkers (value) {\n portalWorker.supervisor.maxWorkingSandboxes = value\n }\n\n get paymentAddress () {\n return portalWorker.supervisor.paymentAddress\n }\n\n set paymentAddress (value) {\n portalWorker.supervisor.paymentAddress = value\n }\n\n get mining () {\n return portalWorker.working\n }\n\n /** Return a special object that represents the current market value\n * @param {number} factor - OPTIONAL multiplier. default: 1.0\n */\n marketValue (factor = 1) {\n return {\n [MARKET_VALUE]: factor\n };\n }\n\n /** Describes the payment required to compute such a slice on a worker\n * or in a market working at the rates described in the WorkValue object.\n * This function does not take into account job-related overhead.\n * \n * @param {object} sliceProfile A SliceProfile-shaped object\n * @param {object} workValue A WorkValue-shaped object\n * @returns {number} Payment value in DCC describing rate to compute slice\n */\n calculateSlicePayment(sliceProfile, workValue) {\n return sliceProfile.cpuHours * workValue.cpuHour\n + sliceProfile.gpuHours * workValue.gpuHour\n + sliceProfile.inputMBytes * workValue.inputMByte\n + sliceProfile.outputMBytes * workValue.outputMByte;\n }\n\n \n /**\n * Deprecated `mine` API. Use `work` instead\n */\n async mine (numberOfCores, paymentAddress = this.paymentAddress) {\n return this.work(numberOfCores, paymentAddress);\n }\n\n /**\n * Instruct the Worker to stop working, then emit workerInactive\n */\n async stopWorking() {\n console.warn('Using the Compute API for controlling workers is deprecated. Use the Worker API instead.')\n await portalWorker.stop(!this.allowSlicesToFinishBeforeStopping)\n }\n\n /**\n * Deprecated `stopMining` API. Use `stopWorking` instead\n */\n stopMining () {\n console.log(\"Warning: used deprecated 'stopMining' API\");\n this.stopWorking();\n }\n \n /**\n * Flag worker as disabled\n */\n disableWorker() {\n disableWorker();\n }\n\n /**\n * Deprecated `disableMiner` API. Use `disableWorker` instead\n */\n disableMiner() {\n this.disableWorker();\n }\n\n /**\n * Experimental replacement for mine() /* wg aug 2019\n * @param {number} [numberOfCores = this.defaultMaxWorkers] - Number of parallel processes to run\n * @param {string} [paymentAddress = this.paymentAddress || wallet.get().address] - The address of the account to work for\n */\n async work(numCores, paymentAddress) {\n console.warn('Using the Compute API for controlling workers is deprecated. Use the Worker API instead.');\n\n portalWorker.supervisor.paymentAddress = paymentAddress || this.paymentAddress || (await wallet.get()).address;\n portalWorker.supervisor.maxWorkingSandboxes = numCores || this.defaultMaxWorkers || 1;\n await portalWorker.start();\n\n return portalWorker;\n }\n\n /**\n * Form 1. Create a Job for the distributed computer by specifying the start,\n * end, and step for a RangeObject with a given work function and arguments.\n *\n * @param {number} start First number in a range (see {@link RangeObject})\n * @param {number} end Last number in a range (see {@link RangeObject})\n * @param {number} [step=1] the space between numbers in a range (see\n * {@link RangeObject})\n * @param {function | string | URL | DcpURL} work Work function as string or\n * function literal.\n * @param {object} [arguments=[]] optional Array-like object which contains\n * arguments that are passed to the work function\n * @returns {Job}\n * @access public\n * @example\n * let job = compute.for(1, 3, function (i) {\n * progress('100%')\n * return i*10\n * })\n */\n /**\n * Form 2.a Create a Job for the distributed computer by specifying the input\n * as a RemoteDataSet with a given work function and arguments.\n *\n * @param {RemoteDataSet} remoteDataSet Array, ES6 function* generator, or any\n * other type of iterable object.\n * \n * Form 2.b Create a Job for the distributed computer by specifying the input\n * as a RemoteDataPattern with a given work function and arguments.\n * @param {RemoteDataPattern} RemoteDataPattern Array, ES6 function* generator, or any\n * other type of iterable object.\n * @param {function | string | URL | DcpURL} work Work function as string or\n * function literal.\n * @param {object} [arguments=[]] optional Array-like object which contains\n * arguments that are passed to the work function\n * @returns {Job}\n * @access public\n * @example\n * const remoteData = ['https://example.com/data'];\n * const job = compute.for(new RemoteDataSet(...remoteData), function(i) {\n * progress(1)\n * return i\n * })\n * \n * * @example\n * const pattern = 'https://example.com/data/{slice}.json';\n * const NumberOfSlices = 5;\n * const job = compute.for(new RemoteDataSet(pattern, NumberOfSlices), function(i) {\n * progress(1)\n * return i\n * })\n */\n /**\n * Form 3. Create a Job for the distributed computer by specifying the input\n * as an Iterable object with a given work function and arguments.\n *\n * @param {Iterable} iterableObject Array, ES6 function* generator, or any\n * other type of iterable object.\n * @param {function | string | URL | DcpURL} work Work function as string or\n * function literal.\n * @param {object} [arguments=[]] optional Array-like object which contains\n * arguments that are passed to the work function\n * @returns {Job}\n * @access public\n * @example\n * let job = compute.for([123,456], function(i) {\n * progress(1)\n * return i\n * })\n */\n /**\n * Form 4. Create a Job for the distributed computer by specifying the input\n * as a RangeObject with a given work function and arguments.\n *\n * @param {RangeObject} rangeObject Either a RangeObject or an object literal\n * which can be used to create a RangeObject (see {@link RangeObject}).\n * @param {function | string | URL | DcpURL} work Work function as string or\n * function literal.\n * @param {object} [arguments=[]] optional Array-like object which contains\n * arguments that are passed to the work function\n * @returns {Job}\n * @access public\n * @example\n * let job = compute.for({start: 10, end: 13, step: 2}, (i) => progress(1) && i)\n */\n /**\n * Form 5. Similar to Form 4, except with a multi range object containing an\n * array of range objects in the key ranges. These are used to create\n * multi-dimensional ranges, like nested loops. If they were written as\n * traditional loops, the outermost loop would be the leftmost range object,\n * and the innermost loop would be the rightmost range object.\n *\n * @param {MultiRangeObject} multiRange Either a MultiRangeObject or any valid\n * argument to the constructor.\n * @param {function | string | URL | DcpURL} work Work function as string or\n * function literal.\n * @param {object} [arguments=[]] optional Array-like object which contains\n * arguments that are passed to the work function\n * @returns {Job}\n * @access public\n * @example\n * let job = compute.for([{start: 1, end: 2}, {start: 3, end: 5}], function(i,j) {\n * return [i, j, i*j]\n * })\n */\n for(...args) {\n // args, or work function\n const lastArg = args[args.length - 1];\n // work function, if args provided\n const secondLastArg = args[args.length - 2];\n let range = null;\n let work;\n let genArgs;\n\n // All forms: extract work and optional args from the end and validate it.\n if (isValidWorkFunctionType(secondLastArg)) {\n // for(..., workFunction, arguments)\n work = secondLastArg\n if (lastArg && Array.from(lastArg).length > 0 || typeof lastArg === 'string') {\n // array or string, use as-is (string should be a URI)\n genArgs = lastArg;\n } else if (lastArg === undefined) {\n genArgs = [];\n } else {\n throw new TypeError(\n 'Work function arguments (the last parameter), should be an array.',\n );\n }\n } else if (isValidWorkFunctionType(lastArg)) {\n // for(..., workFunction)\n work = lastArg;\n genArgs = [];\n }\n\n if (!isWorkFunctionValid(work)) {\n throw new TypeError(\n `Work parameter must be a function or a string that evaluates to a function, or a URL that points to the location of a function.\\nReceived type ${typeof work} (${work})`,\n );\n }\n\n // Extract the proper information from every form.\n const [firstArg, secondArg, thirdArg] = args;\n if (['object', 'string'].includes(typeof firstArg)) {\n // Forms 2 & 3 (data is iterable, eg. an Array or RemoteDataSet)\n // Forms 4 & 5 (data is RangeObject & Friends)\n range = rehydrateRange(firstArg);\n } else if (typeof firstArg === 'number' && typeof secondArg === 'number') {\n // Form 1 (data is start, end[, step])\n const start = firstArg;\n const end = secondArg;\n\n /**\n * If there is an argument between the end and the work fn, use it as step\n * otherwise set step to undefined.\n */\n const step = typeof thirdArg === 'number' ? thirdArg : undefined;\n range = new RangeObject(start, end, step);\n } else {\n throw new TypeError(\n 'Parameters do not match one of the accepted call forms.',\n );\n }\n\n if (range.length === 0) {\n throw new Error('Length of input set must be greater than zero.');\n }\n\n debug('Work:', work);\n debug('Input Set:', range);\n debug('Arguments:', genArgs);\n return new Job(work, range, genArgs);\n\n /**\n * @param {any} workFunction\n */\n function isValidWorkFunctionType(workFunction) {\n return (\n typeof workFunction === 'function' ||\n typeof workFunction === 'string' ||\n DcpURL.isURL(workFunction)\n );\n }\n\n /**\n * Checks if the workFunction can be evaluated to a function or if it's\n * protocol is HTTPS,\n * @param {function | string | URL | DcpURL} workFunction\n */\n function isWorkFunctionValid(workFunction) {\n switch (typeof workFunction) {\n case 'function':\n return true;\n case 'string': {\n return isValidFunction(workFunction);\n }\n case 'object':\n return DcpURL.isURL(workFunction);\n default:\n return false;\n }\n }\n\n /**\n * Evaluates to true if the string can be coerced into a javascript\n * function.\n * @param {string} workFunction\n */\n function isValidFunction(workFunction) {\n try {\n const fn = indirectEval(`(${workFunction})`);\n return typeof fn === 'function';\n } catch (e) {\n return false;\n }\n }\n }\n\n /**\n * Form 1.\n * Create a job which will run a work function once\n *\n * @param {function | string | URL | DcpURL} work Work function as string or function literal.\n * @param {object} [workFunctionArgs=[]] optional Array-like object which contains arguments which are passed to the work func\n * @returns {Job}\n * @access public\n * @example\n * const job = compute.do(function (i) {\n * progress('100%')\n * return i*10\n * })\n */\n /**\n * Form 2.\n * Create a job which will run a work function on the values 1..n\n *\n * @param {number} n Number of times to run the work function.\n * @param {function | string | URL | DcpURL} work Work function as string or function literal.\n * @param {object} [workFunctionArgs=[]] optional Array-like object which contains arguments which are passed to the work func\n * @returns {Job}\n * @access public\n * @example\n * const job = compute.do(5, function (i) {\n * progress('100%')\n * return i*10\n * })\n */\n do(...args) {\n let n;\n let work;\n let workFunctionArgs;\n if (typeof args[0] !== 'number') {\n // Form 1.\n n = 1;\n [work, workFunctionArgs] = args;\n } else {\n // Form 2.\n [n, work, workFunctionArgs] = args;\n }\n return this.for(0, n - 1, work, workFunctionArgs);\n }\n\n /** Cancel a running job, by opaque id\n *\n * @param {string} id Address of the job to cancel.\n * @param {wallet.Keystore} ownerKeystore The keystore of the job owner\n * @param {string} reason If provided, will be sent on to client\n * @returns {object} The status of the funds that were released from escrow.\n * @throws when id or ownerKeystore are not provided.\n * @access public\n */\n async cancel (id, ownerKeystore, reason = '') {\n if (typeof id !== 'string') {\n throw new Error(`compute.cancel: Job ID must be a string, not ${typeof id}.`);\n }\n\n if (!(ownerKeystore instanceof wallet.Keystore)) {\n throw new Error('compute.cancel: ownerKeystore must be instance of wallet.Keystore');\n }\n\n if (!this.deployConnection)\n this.openDeployConn();\n\n const response = await this.deployConnection.send('cancelJob', {\n job: id,\n 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 } else if (typeof firstArg === 'object') {\n // Form 2\n if (\n (typeof firstArg.startTime !== 'undefined' &&\n !(firstArg.startTime instanceof Date)) ||\n (typeof firstArg.endTime !== 'undefined' &&\n !(firstArg.endTime instanceof Date))\n ) {\n throw new TypeError(\n 'startTime and or endTime were not specified as instances of Date',\n );\n }\n\n requestPayload.startDeployTime = firstArg.startTime;\n requestPayload.endDeployTime = firstArg.endTime;\n } else {\n throw new TypeError(`Unknown first argument type ${typeof firstArg}`);\n }\n\n if (!this.deployConnection)\n this.openDeployConn();\n\n const {\n success,\n payload: responsePayload,\n } = await this.deployConnection.send(\n 'listJobs',\n requestPayload,\n ownerKeystore,\n );\n\n if (!success) {\n throw new DCPError('Request for job status failed', responsePayload);\n }\n\n if (typeof firstArg === 'string' || firstArg instanceof Job) {\n return responsePayload[0];\n }\n\n this.deployConnection.off('close', this.deployConnection)\n await this.deployConnection.close();\n\n return responsePayload;\n }\n\n /**\n * Returns information for the job with the provided ID. It will use\n * `wallet.getId()` to retrieve the auth keystore.\n *\n * @param {string} jobAddress\n * @returns {Promise<object>} status object describing a given job\n * @access public\n */\n async getJobInfo(jobAddress) {\n if (!this.phemeConnection)\n this.openPhemeConn();\n\n const ks = await wallet.getId();\n const { payload: jobResponsePayload } = await this.phemeConnection.send('fetchJobReport', \n {\n jobOwner: ks.address,\n job: jobAddress\n });\n\n const { payload: sliceResponsePayload } = await this.phemeConnection.send(\n 'fetchSliceReport', \n {\n jobOwner: ks.address,\n job: jobAddress\n },\n );\n\n const jobReport = {};\n Object.assign(jobReport, jobResponsePayload, sliceResponsePayload);\n\n this.phemeConnection.off('close', this.phemeConnection)\n await this.phemeConnection.close();\n\n return jobReport;\n }\n\n /**\n * Returns information for the slices of the job with the provided ID. It will\n * use `wallet.getId()` to retrieve the auth keystore.\n *\n * @param {string} jobAddress\n * @returns {Promise<object>} A slice status object for the given job.\n * @access public\n */\n async getSliceInfo(jobAddress) {\n if (!this.phemeConnection)\n this.openPhemeConn();\n\n const identityKeystore = await wallet.getId();\n const {\n success,\n payload: responsePayload,\n } = await this.phemeConnection.send(\n 'fetchSliceReport',\n {\n job: jobAddress,\n jobOwner: identityKeystore.address,\n },\n identityKeystore,\n );\n\n if (!success) {\n throw new DCPError('Request for slice info failed', responsePayload);\n }\n\n this.phemeConnection.off('close', this.phemeConnection)\n await this.phemeConnection.close();\n\n return responsePayload;\n }\n}\n\nexports.RemoteDataSet = RemoteDataSet;\nexports.RemoteDataPattern = RemoteDataPattern;\nexports.MARKET_VALUE = MARKET_VALUE;\nexports.compute = new Compute(); /* :( */\nexports.compute.getPublicComputeGroupId = () => __webpack_require__(/*! dcp/common/scheduler-constants */ \"./src/common/scheduler-constants.js\").computeGroups.public.id;\nexports.compute.getPublicComputeGroupDescriptor = () => __webpack_require__(/*! dcp/common/scheduler-constants */ \"./src/common/scheduler-constants.js\").computeGroups.public;\nexports.compute.ResultHandle = ResultHandle; // export as compute.ResultHandle for instanceof checking\nexports.version = {\n api: '1.5.2',\n provides: '1.0.0' /* dcpConfig.scheduler.compatibility.operations.compute */\n};\n\n\n//# sourceURL=webpack:///./src/dcp-client/compute.js?");
|
|
5062
|
+
eval("/**\n * @file Module that implements Compute API\n * @module dcp/compute\n * @access public\n */\n\n/* global dcpConfig */\n\n/**\n * @access private - Will change soon!\n * @typedef {object} JobRequirements\n * @property {object} environment\n * @property {boolean} environment.fdlibm - Workers express capability 'fdlibm' in order for client applications to have confidence that results will be bitwise identical across workers. This library is recommended, but not required, for implementaiton of Google chrome, Mozilla Firefox and the DCP standalone worker use fdlibm but Microsoft Edge and Apple Safari do not use it.\n * @property {boolean} environment.offscreenCanvas When present, this capability indicates that the worker environment has the OffscreenCanvas symbol defined.\n * @property {object} details\n * @property {object} details.offscreenCanvas\n * @property {boolean} details.offscreenCanvas.bigTexture4096 This capability indicates that the worker's WebGL MAX_TEXTURE_SIZE is at least 4096.\n * @property {boolean} details.offscreenCanvas.bigTexture8192 This capability indicates that the worker's WebGL MAX_TEXTURE_SIZE is at least 8192.\n * @property {boolean} details.offscreenCanvas.bigTexture16384 This capability indicates that the worker's WebGL MAX_TEXTURE_SIZE is at least 16384.\n * @property {boolean} details.offscreenCanvas.bigTexture32768 This capability indicates that the worker's WebGL MAX_TEXTURE_SIZE is at least 32768.\n * @property {object} engine\n * @property {boolean} engine.es7 This capability enforces that the worker is running an es7-compliant JavaScript engine.\n * @property {boolean} engine.spidermonkey The capability enforces that the worker will run in the SpiderMonkey JS engine.\n * @property {boolean} browser.chrome The capability to exclude Chrome browsers.\n */\n\n/**\n * This JSDoc 'namespace' is a convenient way to document some globals that are available only in\n * the sandbox environment provided to work functions.\n * @access public\n * @namespace sandboxEnv\n */\n\n/**\n * This function emits a progress event. Progress events should be emitted approximately once per second; a task which fails to emit a \n * progress event for a certain period of time will be cancelled by the supervisor. The argument to this function is interpreted to six \n * significant digits, and must increase for every call. *All work functions must emit at least one progress event* - this requirement \n * will be enforced by the estimator. * The period of time mentioned above will be at least 30 wall-clock seconds and at least 30 \n * benchmark-adjusted seconds\n * @access public\n * @method progress\n * @param {string|number|undefined} n a number between 0 and 1 (inclusive) that represents a best-guess \n * at the completed portion of the task as a ratio of completed work to total work. If the argument is a string ending in the `%` symbol, \n * it will be interpreted as a number in the usual mathematical sense. If the argument is `undefined`, the slice progress is noted but \n * the amount of progress is considered to be indeterminate.\n * @memberof module:dcp/compute~sandboxEnv\n * @returns {boolean} true\n */\n\n/**\n * Emit a `console` event to the `JobHandle` in the client. If the sandbox is running in an environment with a native console object, the native method may also be invoked.\n * \n * @member console\n * @property {function} log fire console event with 'log' as `level` (see {@link Job#event:console|Job#event:console.level}).\n * @property {function} debug fire console event with 'debug' as `level` (see {@link Job#event:console|Job#event:console.level}).\n * @property {function} info fire console event with 'info' as `level` (see {@link Job#event:console|Job#event:console.level}).\n * @property {function} warn fire console event with 'warn' as `level` (see {@link Job#event:console|Job#event:console.level}).\n * @property {function} error fire console event with 'error' as `level` (see {@link Job#event:console|Job#event:console.level}).\n * \n * **Note** some ES environments (Chrome, Firefox) implement C-style print formatting in this method. This is currently not supported.\n * @memberof module:dcp/compute~sandboxEnv\n * @emits Job#console\n * @access public\n */\n\n/**\n * @member work An object that contains the following properties\n * @property {function} emit (eventName, ...) - send arbitrary args with `eventName`. This event is received client-side with those args (see {@link Job#work}).\n * @property {object} job\n * @property {object} job.public Property set on job handle, see {@link Job#public}.\n * @memberof module:dcp/compute~sandboxEnv\n * @access public\n */\n\n/**\n * @member {OffscreenCanvas} OffscreenCanvas As defined in the {@link https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvas|HTML Standard}, provides a canvas which can be rendered off-screen. If this interface is not available in a given worker, the worker will not report capability \"offscreenCanvas\".\n * @memberof module:dcp/compute~sandboxEnv\n * @access public\n */\n\nconst protocolV4 = __webpack_require__(/*! dcp/protocol-v4 */ \"./src/protocol-v4/index.js\");\nconst { EventEmitter } = __webpack_require__(/*! dcp/common/dcp-events/event-emitter */ \"./src/common/dcp-events/event-emitter.js\");\nconst { RangeObject, rehydrateRange } = __webpack_require__(/*! dcp/dcp-client/range-object */ \"./src/dcp-client/range-object.js\");\nconst { RemoteDataSet } = __webpack_require__(/*! dcp/dcp-client/remote-data-set */ \"./src/dcp-client/remote-data-set.js\");\nconst { RemoteDataPattern } = __webpack_require__(/*! dcp/dcp-client/remote-data-pattern */ \"./src/dcp-client/remote-data-pattern.js\");\nconst { Job, ResultHandle } = __webpack_require__(/*! dcp/dcp-client/job */ \"./src/dcp-client/job/index.js\");\nconst wallet = __webpack_require__(/*! dcp/dcp-client/wallet */ \"./src/dcp-client/wallet/index.js\");\nconst { DcpURL } = __webpack_require__(/*! dcp/common/dcp-url */ \"./src/common/dcp-url.js\");\nconst { disableWorker } = __webpack_require__(/*! dcp/dcp-client/worker */ \"./src/dcp-client/worker/index.js\");\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\nconst debugging = __webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope('dcp-client');\n\nconst debug = (...args) => {\n if (debugging()) {\n args.unshift('dcp-client:compute');\n console.debug(...args);\n }\n};\n\nconst MARKET_VALUE = Symbol('Market Value');\nconst indirectEval = eval; // eslint-disable-line no-eval\n\n/**\n * Instance methods of this class are exported as static members of {@link module:dcp/compute|dcp/compute}.\n * @example\n * let job = compute.for(1, 3, function (i) {\n * progress('100%');\n * return i*10;\n * })\n * let results = await job.exec();\n */\nclass Compute extends EventEmitter {\n constructor () {\n super('Compute');\n\n /** @todo move these into some central config */\n this.defaultMaxGPUs = 1;\n this.GPUsAssigned = 0;\n this.allowSlicesToFinishBeforeStopping = false;\n\n this.schedulerURL = undefined; // Will default to dcpConfig.scheduler.location if not overidden\n this.bankURL = undefined; // Will default to dcpConfig.bank.services.bankTeller.location if not overidden\n\n Object.defineProperty(this.marketValue, MARKET_VALUE, {\n value: true,\n writable: false,\n configurable: false,\n enumerable: false,\n });\n\n const ceci = this;\n this.RemoteDataSet = RemoteDataSet;\n this.RemoteDataPattern = RemoteDataPattern;\n // Initialize to null so these properties are recognized for the Job class\n this.phemeConnection = null;\n this.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 } else if (typeof firstArg === 'object') {\n // Form 2\n if (\n (typeof firstArg.startTime !== 'undefined' &&\n !(firstArg.startTime instanceof Date)) ||\n (typeof firstArg.endTime !== 'undefined' &&\n !(firstArg.endTime instanceof Date))\n ) {\n throw new TypeError(\n 'startTime and or endTime were not specified as instances of Date',\n );\n }\n\n requestPayload.startDeployTime = firstArg.startTime;\n requestPayload.endDeployTime = firstArg.endTime;\n } else {\n throw new TypeError(`Unknown first argument type ${typeof firstArg}`);\n }\n\n if (!this.deployConnection)\n this.openDeployConn();\n\n const {\n success,\n payload: responsePayload,\n } = await this.deployConnection.send(\n 'listJobs',\n requestPayload,\n ownerKeystore,\n );\n\n if (!success) {\n throw new DCPError('Request for job status failed', responsePayload);\n }\n\n if (typeof firstArg === 'string' || firstArg instanceof Job) {\n return responsePayload[0];\n }\n\n this.deployConnection.off('close', this.deployConnection)\n await this.deployConnection.close();\n\n return responsePayload;\n }\n\n /**\n * Returns information for the job with the provided ID. It will use\n * `wallet.getId()` to retrieve the auth keystore.\n *\n * @param {string} jobAddress\n * @returns {Promise<object>} status object describing a given job\n * @access public\n */\n async getJobInfo(jobAddress) {\n 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:///./src/dcp-client/compute.js?");
|
|
5063
5063
|
|
|
5064
5064
|
/***/ }),
|
|
5065
5065
|
|
|
@@ -5070,7 +5070,7 @@ eval("/**\n * @file Module that implements Compute API\n * @module dcp/comput
|
|
|
5070
5070
|
/*! no static exports found */
|
|
5071
5071
|
/***/ (function(module, exports, __webpack_require__) {
|
|
5072
5072
|
|
|
5073
|
-
eval("/* WEBPACK VAR INJECTION */(function(module) {/**\n * @file dcp-client-bundle-src.js\n * Top-level file which gets webpacked into the bundle consumed by dcp-client 2.5\n * @author Wes Garland, wes@kingsds.network\n * @date July 2019\n */\n\n{\n let thisScript = typeof document !== 'undefined' ? (typeof document.currentScript !== 'undefined' && document.currentScript) || document.getElementById('_dcp_client_bundle') : {}\n let realModuleDeclare\n\n if ( false || typeof module.declare === 'undefined') {\n realModuleDeclare = ( true) ? module.declare : undefined\n if (false) {}\n module.declare = function moduleUnWrapper (deps, factory) {\n factory(null, module.exports, module)\n return module.exports\n }\n }\n\n let _debugging = () => false\n dcpConfig.future = __webpack_require__(/*! ../common/config-future.js */ \"./src/common/config-future.js\").futureFactory(_debugging, dcpConfig);\n\n /* These modules are official API and must be part of DCP Client */\n let officialApi = {\n 'protocol': __webpack_require__(/*! ../protocol-v4 */ \"./src/protocol-v4/index.js\"),\n 'compute': __webpack_require__(/*! ./compute */ \"./src/dcp-client/compute.js\").compute,\n 'worker': __webpack_require__(/*! ./worker */ \"./src/dcp-client/worker/index.js\"),\n 'wallet': __webpack_require__(/*! ./wallet */ \"./src/dcp-client/wallet/index.js\"),\n };\n\n /* Allow client programs to use modules which happen to be in the bundle anyhow */\n let conveniencePeers = {\n 'ethereumjs-tx': __webpack_require__(/*! ./wallet/keystore */ \"./src/dcp-client/wallet/keystore.js\")._internalEth.tx,\n 'ethereumjs-wallet': __webpack_require__(/*! ./wallet/keystore */ \"./src/dcp-client/wallet/keystore.js\")._internalEth.wallet,\n 'ethereumjs-util': __webpack_require__(/*! ./wallet/keystore */ \"./src/dcp-client/wallet/keystore.js\")._internalEth.util,\n 'socket.io-client': __webpack_require__(/*! socket.io-client */ \"./node_modules/socket.io-client/build/index.js\"),\n 'bignumber.js': __webpack_require__(/*! bignumber.js */ \"./node_modules/bignumber.js/bignumber.js\"),\n 'semver': __webpack_require__(/*! semver */ \"./node_modules/semver/semver.js\"),\n };\n\n /* Some of these modules are API-track. Some of them need to be published to be\n * available for top-level resolution by DCP internals. Those (mostly) should have\n * been written using relative module paths.....\n */\n let modules = Object.assign({\n 'dcp-build': {\"version\":\"
|
|
5073
|
+
eval("/* WEBPACK VAR INJECTION */(function(module) {/**\n * @file dcp-client-bundle-src.js\n * Top-level file which gets webpacked into the bundle consumed by dcp-client 2.5\n * @author Wes Garland, wes@kingsds.network\n * @date July 2019\n */\n\n{\n let thisScript = typeof document !== 'undefined' ? (typeof document.currentScript !== 'undefined' && document.currentScript) || document.getElementById('_dcp_client_bundle') : {}\n let realModuleDeclare\n\n if ( false || typeof module.declare === 'undefined') {\n realModuleDeclare = ( true) ? module.declare : undefined\n if (false) {}\n module.declare = function moduleUnWrapper (deps, factory) {\n factory(null, module.exports, module)\n return module.exports\n }\n }\n\n let _debugging = () => false\n dcpConfig.future = __webpack_require__(/*! ../common/config-future.js */ \"./src/common/config-future.js\").futureFactory(_debugging, dcpConfig);\n\n /* These modules are official API and must be part of DCP Client */\n let officialApi = {\n 'protocol': __webpack_require__(/*! ../protocol-v4 */ \"./src/protocol-v4/index.js\"),\n 'compute': __webpack_require__(/*! ./compute */ \"./src/dcp-client/compute.js\").compute,\n 'worker': __webpack_require__(/*! ./worker */ \"./src/dcp-client/worker/index.js\"),\n 'wallet': __webpack_require__(/*! ./wallet */ \"./src/dcp-client/wallet/index.js\"),\n };\n\n /* Allow client programs to use modules which happen to be in the bundle anyhow */\n let conveniencePeers = {\n 'ethereumjs-tx': __webpack_require__(/*! ./wallet/keystore */ \"./src/dcp-client/wallet/keystore.js\")._internalEth.tx,\n 'ethereumjs-wallet': __webpack_require__(/*! ./wallet/keystore */ \"./src/dcp-client/wallet/keystore.js\")._internalEth.wallet,\n 'ethereumjs-util': __webpack_require__(/*! ./wallet/keystore */ \"./src/dcp-client/wallet/keystore.js\")._internalEth.util,\n 'socket.io-client': __webpack_require__(/*! socket.io-client */ \"./node_modules/socket.io-client/build/index.js\"),\n 'bignumber.js': __webpack_require__(/*! bignumber.js */ \"./node_modules/bignumber.js/bignumber.js\"),\n 'semver': __webpack_require__(/*! semver */ \"./node_modules/semver/semver.js\"),\n };\n\n /* Some of these modules are API-track. Some of them need to be published to be\n * available for top-level resolution by DCP internals. Those (mostly) should have\n * been written using relative module paths.....\n */\n let modules = Object.assign({\n 'dcp-build': {\"version\":\"afa05a15457fc3feb611804dc4f2159bba2ff452\",\"branch\":\"release\",\"dcpClient\":{\"version\":\"4.1.19\",\"from\":\"git+ssh://git@gitlab.com/Distributed-Compute-Protocol/dcp-client.git#release\",\"resolved\":\"git+ssh://git@gitlab.com/Distributed-Compute-Protocol/dcp-client.git#bad4562e53079f9ed48400c7d25be63d429ba36c\"},\"built\":\"Tue Feb 08 2022 11:12:59 GMT-0500 (Eastern Standard Time)\",\"config\":{\"generated\":\"Tue 08 Feb 2022 11:12:57 AM EST by erose on lorge\",\"build\":\"debug\"},\"webpack\":\"4.46.0\",\"node\":\"v12.22.10\"},\n 'dcp-xhr': __webpack_require__(/*! ../common/dcp-xhr */ \"./src/common/dcp-xhr.js\"),\n 'dcp-env': __webpack_require__(/*! ../common/dcp-env */ \"./src/common/dcp-env.js\"),\n 'dcp-url': __webpack_require__(/*! ../common/dcp-url */ \"./src/common/dcp-url.js\"),\n 'cli': __webpack_require__(/*! ../common/cli */ \"./src/common/cli.js\"),\n 'dcp-timers': __webpack_require__(/*! ../common/dcp-timers */ \"./src/common/dcp-timers.js\"),\n 'dcp-dot-dir': __webpack_require__(/*! ../common/dcp-dot-dir */ \"./src/common/dcp-dot-dir.js\"),\n 'dcp-assert': __webpack_require__(/*! ../common/dcp-assert */ \"./src/common/dcp-assert.js\"),\n 'dcp-events': __webpack_require__(/*! ../common/dcp-events */ \"./src/common/dcp-events/index.js\"),\n 'utils': __webpack_require__(/*! ../utils */ \"./src/utils/index.js\"),\n 'debugging': __webpack_require__(/*! ../debugging */ \"./src/debugging.js\"),\n 'publish': __webpack_require__(/*! ../common/dcp-publish */ \"./src/common/dcp-publish.js\"),\n 'compute-groups': {\n ...__webpack_require__(/*! ./compute-groups */ \"./src/dcp-client/compute-groups/index.js\"),\n publicGroupOpaqueId: __webpack_require__(/*! ../common/scheduler-constants */ \"./src/common/scheduler-constants.js\").computeGroups.public.opaqueId,\n },\n 'bank-util': __webpack_require__(/*! ./bank-util */ \"./src/dcp-client/bank-util.js\"),\n 'protocol-v4': __webpack_require__(/*! ../protocol-v4 */ \"./src/protocol-v4/index.js\"), /* deprecated */\n 'client-modal': __webpack_require__(/*! ./client-modal */ \"./src/dcp-client/client-modal/index.js\"),\n 'legacy-modal': __webpack_require__(/*! ../../portal/www/js/modal */ \"./portal/www/js/modal.js\").Modal,\n 'eth': __webpack_require__(/*! ./wallet/eth */ \"./src/dcp-client/wallet/eth.js\"),\n 'serialize': __webpack_require__(/*! ../utils/serialize */ \"./src/utils/serialize.js\"),\n 'job': __webpack_require__(/*! ./job */ \"./src/dcp-client/job/index.js\"),\n 'range-object': __webpack_require__(/*! ./range-object */ \"./src/dcp-client/range-object.js\"),\n 'stats-ranges': __webpack_require__(/*! ./stats-ranges */ \"./src/dcp-client/stats-ranges.js\"),\n 'standard-objects': {}\n }, conveniencePeers, officialApi);\n\n /* Export the JS Standard Classes (etc) from the global object of the bundle evaluation context,\n * in case we have code somewhere that needs to use these for instanceof checks.\n */\n ;[ Object, Function, Boolean, Symbol,\n Error, EvalError, RangeError, ReferenceError, SyntaxError, TypeError, URIError,\n Number, Math, Date,\n String, RegExp,\n Array, Int8Array, Uint8Array, Uint8ClampedArray, Int16Array, Uint16Array, Int32Array, Uint32Array, Float32Array, Float64Array,\n Map, Set, WeakMap, WeakSet,\n ArrayBuffer, DataView, JSON,\n Promise, \n Reflect, Proxy, Intl, WebAssembly, __webpack_require__\n ].forEach(function (obj) {\n if (obj.name && (typeof obj === 'function' || typeof obj === 'object'))\n modules['standard-objects'][obj.name] = obj\n })\n\n if (typeof BigInt !== 'undefined')\n modules['standard-objects']['BigInt'] === BigInt;\n if (typeof BigInt64Array !== 'undefined')\n modules['standard-objects']['BigInt64Array'] === BigInt64Array;\n if (typeof BigInt64Array !== 'undefined')\n modules['standard-objects']['BigUint64Array'] === BigUint64Array;\n\n module.declare([], function(require, exports, module) {\n Object.assign(exports, modules)\n exports['dcp-config'] = dcpConfig\n })\n if (realModuleDeclare)\n module.declare = realModuleDeclare\n\n bundleExports = thisScript.exports = exports; /* must be last expression evaluated! */\n}\n\n/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(/*! ./../../node_modules/webpack/buildin/module.js */ \"./node_modules/webpack/buildin/module.js\")(module)))\n\n//# sourceURL=webpack:///./src/dcp-client/index.js?");
|
|
5074
5074
|
|
|
5075
5075
|
/***/ }),
|
|
5076
5076
|
|
|
@@ -5082,7 +5082,7 @@ eval("/* WEBPACK VAR INJECTION */(function(module) {/**\n * @file dcp-cli
|
|
|
5082
5082
|
/***/ (function(module, exports, __webpack_require__) {
|
|
5083
5083
|
|
|
5084
5084
|
"use strict";
|
|
5085
|
-
eval("/**\n * @file job/index.js\n * @author Eddie Roosenmaallen, eddie@kingsds.network\n * Matthew Palma, mpalma@kingsds.network\n * @date November 2018\n *\n * This module implements the Compute API's Job Handle\n *\n */\n\n/** @typedef {import('dcp/dcp-client/wallet/keystore').Keystore} Keystore */\n\n\nconst BigNumber = __webpack_require__(/*! bignumber.js */ \"./node_modules/bignumber.js/bignumber.js\");\nconst { v4: uuidv4 } = __webpack_require__(/*! uuid */ \"./node_modules/uuid/dist/esm-browser/index.js\");\nconst { EventEmitter, PropagatingEventEmitter } = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\");\nconst { RangeObject, MultiRangeObject, DistributionRange, SuperRangeObject } = __webpack_require__(/*! dcp/dcp-client/range-object */ \"./src/dcp-client/range-object.js\");\nconst { fetchURI, encodeDataURI, dumpObject } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\nconst { getTextEncoder } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\nconst serialize = __webpack_require__(/*! dcp/utils/serialize */ \"./src/utils/serialize.js\");\nconst wallet = __webpack_require__(/*! dcp/dcp-client/wallet */ \"./src/dcp-client/wallet/index.js\");\nconst protocolV4 = __webpack_require__(/*! dcp/protocol-v4 */ \"./src/protocol-v4/index.js\");\nconst { EventSubscriber } = __webpack_require__(/*! dcp/events/event-subscriber */ \"./src/events/event-subscriber.js\");\nconst { DcpURL } = __webpack_require__(/*! dcp/common/dcp-url */ \"./src/common/dcp-url.js\");\nconst ClientModal = __webpack_require__(/*! dcp/dcp-client/client-modal */ \"./src/dcp-client/client-modal/index.js\");\nconst { Worker } = __webpack_require__(/*! dcp/dcp-client/worker */ \"./src/dcp-client/worker/index.js\");\nconst { RemoteDataSet } = __webpack_require__(/*! dcp/dcp-client/remote-data-set */ \"./src/dcp-client/remote-data-set.js\");\nconst { RemoteDataPattern } = __webpack_require__(/*! dcp/dcp-client/remote-data-pattern */ \"./src/dcp-client/remote-data-pattern.js\");\nconst { ResultHandle } = __webpack_require__(/*! ./result-handle */ \"./src/dcp-client/job/result-handle.js\");\nconst { SlicePaymentOffer } = __webpack_require__(/*! ./slice-payment-offer */ \"./src/dcp-client/job/slice-payment-offer.js\");\nconst DCP_ENV = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\nconst dcpPublish = __webpack_require__(/*! dcp/common/dcp-publish */ \"./src/common/dcp-publish.js\");\nconst computeGroups = __webpack_require__(/*! dcp/dcp-client/compute-groups */ \"./src/dcp-client/compute-groups/index.js\");\nconst schedulerConstants = __webpack_require__(/*! dcp/common/scheduler-constants */ \"./src/common/scheduler-constants.js\");\nconst { 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\");\n\nconst TextEncoder = getTextEncoder();\nlet dannyDebugCounter = 0;\n\nconst log = (...args) => {\n if (debugging('job')) {\n console.debug('dcp-client:job', ...args);\n }\n};\n\nconst ON_BROWSER = DCP_ENV.isBrowserPlatform;\nconst sideloaderModuleIdentifier = 'sideloader-v1';\n\n// Symbols used to hide private members and functions on the Job instance\nconst debugBuild = (__webpack_require__(/*! dcp/common/dcp-build */ \"./src/common/dcp-build.js\").build === 'debug');\nconst INTERNAL_SYMBOL = debugBuild ? '_' : Symbol('Job Internals');\nconst SNAPSHOT = debugBuild ? '_snapshot' : Symbol('Job.snapshot');\nconst DEPLOY_JOB = debugBuild ? '_deploy' : Symbol('Job.deploy');\n\nconst ADD_LISTENERS = Symbol('Job.addListeners');\nconst LISTEN_TO_EVENTS = Symbol('Job.listenToEvents');\nconst LISTEN_TO_WORK_EVENTS = Symbol('Job.listenToWorkEvents');\nconst ON_RESULT = Symbol('Job.onResult');\nconst ON_STATUS = Symbol('Job.onStatus');\n\nexports.JOB_INTERNAL_SYMBOL = INTERNAL_SYMBOL; /* allow friends to access our guts, eg. job/result-handle */\n\nconst DEFAULT_REQUIREMENTS = {\n engine: {\n es7: null,\n spidermonkey: null\n },\n environment: {\n webgpu: null,\n offscreenCanvas: null,\n fdlibm: null\n },\n browser: {\n chrome: null\n },\n details: {\n offscreenCanvas: {\n bigTexture4096: null,\n bigTexture8192: null,\n bigTexture16384: null,\n bigTexture32768: null,\n }\n },\n discrete: null,\n};\nconst ZERO_COST_HOLD_ADDRESS = '0x' + '0'.repeat(130);\n\n/** @typedef {import('../range-object').RangeLike} RangeLike */\n\n/**\n * Ensure input data is an appropriate format\n * @param {RangeObject | DistributionRange | RemoteDataSet | Array | Iterable}\n * inputData - A URI-shaped string, a [Multi]RangeObject-constructing value, or\n * an array of slice data\n * @return {RangeObject | RangeLike | DistributionRange | RemoteDataSet | Array}\n * The coerced input in an appropriate format ([Multi]RangeObject,\n * DistributionRange, RemoteDataSet, or array)\n */\nconst wrangleData = (inputData) => {\n if (typeof inputData === 'object' && !!inputData.ranges) { return new MultiRangeObject(inputData) }\n\n if (RangeObject.isRangelike(inputData)) { return inputData }\n if (RangeObject.isRangeObject(inputData)) { return inputData }\n if (DistributionRange.isDistribution(inputData)) { return inputData }\n if (RangeObject.isProtoRangelike(inputData)) { return new RangeObject(inputData) }\n if (DistributionRange.isProtoDistribution(inputData)) { return new DistributionRange(inputData) }\n if (RemoteDataSet.isRemoteDataSet(inputData)) { return inputData }\n if (RemoteDataPattern.isRemoteDataPattern(inputData)) { return inputData }\n\n return Array.isArray(inputData) ? inputData : [inputData];\n};\n\n// Used to validate the requirements object,\n// applies the default requirements schema\nconst applyObjectSchema = (obj, schema, errContext='', scope='') => {\n let checkedObjs = [];\n\n for (let p in schema) {\n let fullPropScope = scope.concat(p);\n if (!(p in obj)) {\n if (typeof schema[p] === 'object' && schema[p] !== null) {\n obj[p] = {};\n checkedObjs.push(fullPropScope);\n applyObjectSchema(obj[p], schema[p], errContext, fullPropScope.concat('.'));\n } else obj[p] = schema[p];\n } else if (typeof schema[p] === 'object' && schema[p] !== null && !checkedObjs.includes(fullPropScope)) {\n if (typeof obj[p] !== 'object') throw new Error(`${errContext}: Schema mismatch, property '${fullPropScope}' should be an object.`);\n else {\n checkedObjs.push(fullPropScope);\n applyObjectSchema(obj[p], schema[p], errContext, fullPropScope.concat('.'));\n }\n } else if ((typeof schema[p] !== 'object' || schema[p] === null)\n && typeof obj[p] !== 'boolean' && obj[p] !== null) {\n throw new Error(`${errContext}: Schema mismatch, object property '${fullPropScope}' should be a boolean.`);\n }\n }\n\n for (let p in obj) {\n let fullPropScope = scope.concat(p);\n if (!(p in schema)) throw new Error(`${errContext}: Schema mismatch, object has extra key '${fullPropScope}'.`);\n else if (typeof obj[p] === 'object' && obj[p] !== null && !checkedObjs.includes(fullPropScope)) {\n checkedObjs.push(fullPropScope);\n applyObjectSchema(obj[p], schema[p], errContext, fullPropScope.concat('.'));\n }\n }\n}\n\n/**\n * @classdesc The Compute API's Job Handle (see {@link https://docs.dcp.dev/specs/compute-api.html#job-handles|Compute API spec})\n * Job handles are objects which correspond to jobs. \n * They are created by some exports of the compute module, such as {@link module:dcp/compute.do|compute.do} and {@link module:dcp/compute.for|compute.for}.\n * @extends module:dcp/dcp-events.PropagatingEventEmitter\n * @hideconstructor\n * @access public\n */\nclass Job extends PropagatingEventEmitter {\n /**\n * This event is emitted when the job is accepted by the scheduler on deploy.\n * \n * @event Job#accepted\n * @access public\n * @type {object}\n * @property {object} job Original object that was delivered to the scheduler for deployment\n *//**\n * Fired when the job is cancelled.\n * \n * @event Job#cancel\n * @access public\n *//**\n * Fired when a result is returned.\n * \n * @event Job#result\n * @access public\n * @type {object}\n * @property {string} jobAddress Address of the job\n * @property {string} task ID of the task (slice) the result came from\n * @property {number} sort The index of the slice\n * @property {object} result\n * @property {string} result.request\n * @property {*} result.result The value returned from the work function\n *//**\n * Fired when the result handle is modified, either when a new `result` event is fired or when the results are populated with `results.fetch()`\n * \n * @event Job#resultsUpdated\n * @access public\n *//**\n * Fired when the job has been completed.\n * \n * @event Job#complete\n * @access public\n * @type {ResultHandle}\n *//**\n * Fired when the job's status changes.\n * \n * @event Job#status\n * @access public\n * @type {object}\n * @property {string} jobAddress Address of the job\n * @property {number} total Total number of slices in the job\n * @property {number} distributed Number of slices that have been distributed\n * @property {number} computed Number of slices that have completed execution (returned a result)\n * @property {string} runStatus Current runStatus of the job\n *//**\n * Fired when a slice throws an error.\n * \n * @event Job#error\n * @access public\n * @type {object}\n * @property {string} jobAddress Address of the job\n * @property {number} sliceIndex Index of the slice that threw the error\n * @property {string} message The error message\n * @property {string} stack The error stacktrace\n * @property {string} name The error type name\n *//**\n * Fired when a slice uses one of the console log functions.\n * \n * @event Job#console\n * @access public\n * @type {object}\n * @property {string} jobAddress Address of the job\n * @property {number} sliceIndex The index of the slice that produced this event\n * @property {string} level The log level, one of `debug`, `info`, `log`, `warn`, or `error`\n * @property {string} message The console log message\n *//**\n * Fired when a slice is stopped for not calling progress. Contains information about how long the slice ran for, and about the last reported progress calls.\n * \n * @event Job#noProgress\n * @access public\n * @type {object}\n * @property {string} jobAddress Address of the job\n * @property {number} sliceIndex The index of the slice that failed due to no progress\n * @property {number} timestamp How long the slice ran before failing\n * @property {object} progressReports\n * @property {object} progressReports.last The last progress report received from the worker\n * @property {number} progressReports.last.timestamp Time since the start of the slice\n * @property {number} progressReports.last.progress Progress value reported\n * @property {*} progressReports.last.value The last value that was passed to the progress function\n * @property {number} progressReports.last.throttledReports Number of calls to progress that were throttled since the last report\n * @property {object} progressReports.lastUpdate The last determinate (update to the progress param) progress report received from the worker\n * @property {number} progressReports.lastUpdate.timestamp\n * @property {number} progressReports.lastUpdate.progress\n * @property {*} progressReports.lastUpdate.value\n * @property {number} progressReports.lastUpdate.throttledReports\n *//**\n * Identical to `noProgress`, except that it also contains the data that the slice was executed with.\n * \n * @event Job#noProgressData\n * @access public\n * @type {object}\n * @property {*} data The data that the slice was executed with\n *//**\n * Fired when the job is paused due to running out of funds. The job can be resumed by escrowing more funds then resuming the job.\n * \n * Event payload is the estimated funds required to complete the job\n * \n * @event Job#ENOFUNDS\n * @access public\n * @type {BigNumber}\n *//**\n * Fired when the job is cancelled due to the work function not calling the `progress` method frequently enough.\n * \n * @event Job#ENOPROGRESS\n * @access public\n *//**\n * The job was cancelled because scheduler has determined that individual tasks in this job exceed the maximum allowable execution time.\n * \n * @event Job#ESLICETOOSLOW\n * @access public\n *//**\n * Fired when the job is cancelled because too many work functions are terminating with uncaught exceptions.\n * \n * @event Job#ETOOMANYERRORS\n * @access public\n */\n\n /**\n * @form1 new Job(job_shaped_object)\n * @form2 new Job('application_worker_address'[, data[, arguments]])\n * @form3b new Job('worker source'[, data[, arguments]])\n * @form3b new Job(worker_function[, data[, arguments]])\n */\n\n constructor() {\n super('Job');\n\n this.readyStateChange = (readyState) => {\n this.readyState = readyState;\n this.emit('readyStateChange', this.readyState);\n };\n this.readyStateChange(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\n // The following public members are only populated after the job has been deployed\n this.address = null;\n this.receipt = null;\n this.meanSliceProfile = null;\n \n /* \n * A number (can be null, undefined, or infinity) describing the estimationSlicesRemaining in the jpd (dcp-2593)\n * @type {number}\n * @access public\n */\n this.estimationSlices = undefined;\n\n /* We avoid using job.id internally because it is easy to confuse with db::jobs.id, but is an API\n * interface that we present to end-user developers so we need to keep it.\n */\n Object.defineProperty(this, 'id', {\n get: () => this.address,\n set: (id) => this.address = id\n });\n \n // \n /**\n * An EventEmitter for custom events dispatched by the work function.\n * @type {module:dcp/dcp-events.EventEmitter}\n * @access public\n * @example\n * // in sandbox\n * work.emit('myEventName', 1, [2], \"three\");\n * // clientside\n * job.work.on('myEventName', (num, arr, string) => { });\n */\n this.work = new EventEmitter('job.work');\n\n //Initialize the eventSubscriber so each job has unique eventSubscriber\n this.eventSubscriber = new EventSubscriber(this);\n \n // Some events can't be emitted 'naturally' without having weird/wrong output.\n // An example of this is results. When results are returned from the scheduler,\n // They come in as a dataURI of kvin-ified results. We need to parse all that before\n // We actually send it to the client. For such events, we will intercept them, parse\n // them as needed, then emit the event with the 'fixed' data to the client.\n \n const ceci = this\n const parseConsole = function deserializeConsoleMessage(ev) {\n ceci.emit('console', ev);\n }\n \n this.eventIntercepts = {\n result: (ev) => this[ON_RESULT](ev),\n status: (ev) => this[ON_STATUS](ev),\n cancel: (ev) => this[INTERNAL_SYMBOL].events.emit('stopped', ev),\n console: parseConsole,\n }\n\n this.eventTypes = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\").eventTypes;\n\n this.work.on('newListener', (evt) => {\n if (!this[INTERNAL_SYMBOL].connected && evt !== 'newListener') {\n this[INTERNAL_SYMBOL].subscribedWorkerEvents.add(evt);\n }\n });\n\n this.on('newListener', (evt) => {\n if (!this[INTERNAL_SYMBOL].connected && evt !== 'newListener') {\n this[INTERNAL_SYMBOL].subscribedEvents.add(evt);\n }\n });\n // Form1: If arguments[0] is an object that looks like a job, this is a 'copy constructor'\n // where we inherit as much as possible from the original instance.\n if (typeof arguments[0] === 'object' &&\n arguments[0].type &&\n arguments[0].data &&\n arguments[0].public) {\n \n let src = arguments[0];\n\n this[INTERNAL_SYMBOL].payloadDetails = {\n ...src,\n data: wrangleData(src.data), // rehydrate ranges\n };\n\n if (src.feeStructure) {\n this.setSlicePaymentOffer(src.feeStructure);\n }\n \n if (src.address)\n this.address = src.address;\n if (src.payloadData.status)\n this[ON_STATUS](this[INTERNAL_SYMBOL].payload.status, false);\n if (src.public)\n Object.assign(this.public, src.public);\n } else {\n /* Forms 2-4 */ \n if (typeof arguments[0] === 'function')\n arguments[0] = arguments[0].toString();\n\n if (typeof arguments[0] === 'string') {\n this[INTERNAL_SYMBOL].workFunctionURI = encodeDataURI(arguments[0], 'application/javascript');\n } else if (DcpURL.isURL(arguments[0])) {\n this[INTERNAL_SYMBOL].workFunctionURI = arguments[0].href;\n } \n\n const wrangledInputData = wrangleData(arguments[1] || []);\n const wrangledArguments = wrangleData(arguments[2] || []);\n \n log('wrangledInputData:', wrangledInputData);\n log('wrangledArguments:', wrangledArguments);\n \n Object.assign(this[INTERNAL_SYMBOL].payloadDetails, {\n request: 'main',\n data: wrangledInputData,\n arguments: wrangledArguments,\n });\n }\n\n // This should happen last, it depends on the this.[INTERNAL_SYMBOL].payloadDetails.data array\n /**\n * A Result Handle object used to query and manipulate the output set. \n * Present once job has been deployed.\n * @type {ResultHandle}\n * @access public\n */\n this.results = new ResultHandle(this);\n\n /**\n * Read-only copy of the job's uuid (generated or rehydrated via form1 constructor)\n */\n Object.defineProperty(this, 'uuid', {\n get: () => uuid,\n configurable: false,\n enumerable: true,\n });\n \n // each entry contains the computeGroupID, joinKey, joinSecret, joinKeystore\n // Add to public compute group by default\n this.computeGroups = [ Object.assign({}, schedulerConstants.computeGroups.public) ];\n\n\n // Initialize to null so these properties are recognized for the Job class\n this.bankConnection = null;\n this.deployConnection = null;\n this.openBankConn = function openBankConn()\n {\n ceci.bankConnection = new protocolV4.Connection(dcpConfig.bank.services.bankTeller);\n ceci.bankConnection.on('close', ceci.openBankConn);\n }\n\n this.openDeployConn = function openDeployConn()\n {\n ceci.deployConnection = new protocolV4.Connection(dcpConfig.scheduler.services.jobSubmit);\n ceci.deployConnection.on('close', ceci.openDeployConn);\n }\n\n this.openBankConn();\n this.openDeployConn();\n }\n\n /** \n * Cancel the job\n * @access public\n * @param {string} reason If provided, will be sent to client\n */\n async cancel (reason = undefined) {\n const response = await this.deployConnection.send('cancelJob', {\n job: this.address,\n owner: this.paymentAccountKeystore.address,\n reason,\n }, this.paymentAccountKeystore);\n\n return response.payload;\n }\n\n /** \n * Resume this job\n * @access public\n */\n async resume() {\n const response = await this.schedulerConnection.send('resumeJob', {\n job: this.address,\n owner: this.paymentAccountKeystore.address,\n }, this.paymentAccountKeystore);\n\n return response.payload;\n }\n\n /**\n * Helper function for retrieving info about the job. The job must have already been deployed.\n * An alias for {@link module:dcp/compute.getJobInfo}.\n * @access public\n */\n async getJobInfo(){\n return await __webpack_require__(/*! ../compute */ \"./src/dcp-client/compute.js\").compute.getJobInfo(this.address);\n }\n\n /**\n * Helper function for retrieving info about the job's slices. The job must have already been deployed.\n * An alias for {@link module:dcp/compute.getSliceInfo}.\n * @access public\n */\n async getSliceInfo(){\n return await __webpack_require__(/*! ../compute */ \"./src/dcp-client/compute.js\").compute.getSliceInfo(this.address);\n }\n \n /**\n * Helper function that tries to upload slicePile to scheduler for the job with the given address\n * If the connection throws, we will continue trying to upload until it has thrown errorTolerance times\n * However, if the upload is unsuccessful, we throw immediately.\n * @param {Array} slicePile \n * @returns payload containing success property (pertaining to success of adding slices to job) as well as lastSliceNumber of job \n */\n async safeSliceUpload(slicePile)\n {\n let payload = undefined; // future return value\n let errorTolerance = dcpConfig.job.sliceUploadErrorTolerance; // copy number of times we will tolerate non-success when uploading slices directly from config\n await this.deployConnection.keepalive();\n while (true) // eslint-disable-line no-constant-condition\n {\n try\n {\n payload = await this.deployConnection.send('addSliceData', {\n job: this.address,\n dataValues: kvin.marshal(slicePile),\n });\n if (!payload.success)\n throw new DCPError('Cannot upload slice data to scheduler','EUPLOADSCHED');\n else\n break;\n }\n catch (error)\n {\n if (--errorTolerance <= 0)\n throw error;\n }\n }\n return payload;\n }\n \n /**\n * This function contains the actual logic behind staggered slice uploads\n * to the scheduler which makes quicker deployment possible.\n * \n * Note that we pass in mostToTake so that the uploadLogic function can update \n * it to the new value it needs to be, and then pass it back to the wrapper \n * function (addSlices) which actually does the work of picking up slices \n * and thus uses this value\n * @param {Array} pile the actual array of slices being uploaded to scheduler\n * @param {Number} mostToTake number of slices that should be taken by the wrapper function (addSlices) \n * which actually does the work of picking up slices and thus uses this value.\n * We pass in mostToTake so that the uploadLogic function can update it to the \n * new value it needs to be, and then pass it back to the wrapper\n * @returns payload containing success property (pertaining to success of adding slices to job) as well as lastSliceNumber of job\n */\n async sliceUploadLogic(pile, mostToTake)\n {\n const slicesTaken = pile.length;\n \n let pileSize = 0; // total size of the pile's slices in bytes\n \n // calculate pileSize by finding sum of bytesizes of each slice (after each is marshalled)\n for (let i = 0; i < slicesTaken; ++i)\n {\n let sliceSize = (new TextEncoder()).encode(kvin.stringify(pile[i])).length;\n pileSize += sliceSize;\n }\n\n let newMostToTake;\n let uploadedSlices;\n \n // if the pile is larger than the ceiling but we only took one slice, there's no smaller pile we can make\n // so we upload it anyway but we don't try taking more next time cause we were over the ceiling (which \n // is a hard limit on upload sizes)\n if ((pileSize > dcpConfig.job.uploadSlicesCeiling) && (slicesTaken === 1))\n {\n uploadedSlices = await this.safeSliceUpload(pile);\n newMostToTake = 1;\n }\n \n // if the pile is larger than the target but we only took one slice, there's no smaller pile we can make\n // so we upload it anyway and still try taking more\n else if ((pileSize > dcpConfig.job.uploadSlicesTarget) && (slicesTaken === 1))\n {\n uploadedSlices = await this.safeSliceUpload(pile);\n newMostToTake = mostToTake * dcpConfig.job.uploadIncreaseFactor;\n }\n \n // otherwise, if the pile is smaller than the soft ceiling, send up the pile anyway (since piles are expensive to make) \n // but remember to include incrementFactor times as many slices in the next pile\n else if (pileSize <= dcpConfig.job.uploadSlicesTarget)\n {\n uploadedSlices = await this.safeSliceUpload(pile);\n newMostToTake = mostToTake * dcpConfig.job.uploadIncreaseFactor;\n }\n \n // if the pile is over the ceiling then we do not upload and begin reassembling our piles from scratch\n else if (pileSize > dcpConfig.job.uploadSlicesCeiling)\n {\n newMostToTake = -1;\n }\n \n // if the pile is over the target (but implicitly under the ceiling), then upload the pile to scheduler but lower mostToTake\n // by a smaller factor than incrementFactor to allow us to begin \"centering\" sizes of piles around the target\n else if (pileSize > dcpConfig.job.uploadSlicesTarget)\n {\n uploadedSlices = await this.safeSliceUpload(pile);\n newMostToTake = Math.ceil(mostToTake / ((2 / 3) * dcpConfig.job.uploadIncreaseFactor));\n }\n\n if (uploadedSlices && uploadedSlices.success && typeof uploadedSlices.payload.lastSliceNumber !== 'undefined')\n // must check if uploadedSlices exists first since if pileSize > ceiling then there will be no uploadedSlices\n this.status.total = uploadedSlices.payload.lastSliceNumber;\n\n let payload = uploadedSlices ? uploadedSlices.payload : undefined;\n return { payload, newMostToTake }; // in case the user needs lastSliceNumber's value\n }\n \n /**\n * Uploads slices to the scheduler in a staggered fashion\n * @param {Array} dataValues actual array of slices being uploaded to scheduler\n * @returns payload containing success property (pertaining to success of adding slices to job) as well as lastSliceNumber of job\n */\n async addSlices(dataValues)\n {\n if (!Array.isArray(dataValues))\n throw new TypeError('Only data-by-value jobs may dynamically add slices');\n\n let mostToTake = 1; // maximum number of slices we could take in per pile\n let payload = undefined; // used in return value\n let slicesTaken = 0; // number of slices in the pile already\n let pile = [];\n \n for (let slice of dataValues)\n {\n pile.push(slice);\n slicesTaken++;\n if (slicesTaken === mostToTake)\n {\n let total = await this.sliceUploadLogic(pile, mostToTake);\n payload = total.payload;\n \n if (total.newMostToTake < 0)\n {\n /* if total.newMostToTake == -1 (only non-positive value returned), then the pile was not successfully\n * uploaded because it was over the ceiling and we need to upload the pile *itself* again, recursively\n */\n payload = await this.addSlices(pile);\n /* and next time, the number of slices we take is the number from this time *divided* by the incrementFactor\n * since we know invariably that number of slices was under the ceiling AND target\n * if you're curious why that's an invariant, this is because mostToTake only ever *increases* by being multiplied by \n * a factor of incrementFactor within sliceUploadLogic, and this only occurs when the pile being uploaded that time\n * was under the target\n */\n mostToTake = mostToTake / dcpConfig.job.uploadIncreaseFactor;\n }\n else\n {\n /* in all other cases (other than the pile size being over the ceiling) the sliceUploadLogic helper \n * determines the number of slices we should pick up next time, so we just use the value it spits out\n */\n mostToTake = total.newMostToTake;\n }\n \n // reset slicesTaken and pile since at this point we know for sure the pile has been uploaded\n pile = [];\n slicesTaken = 0;\n }\n else\n {\n continue;\n }\n }\n // upload the pile one last time in case we continued off the last slice with a non-empty pile\n if (pile.length !== 0)\n {\n let finalObj = await this.sliceUploadLogic(pile, mostToTake);\n payload = finalObj.payload;\n mostToTake = finalObj.newMostToTake;\n \n if (mostToTake < 0)\n {\n // if you need documentation on the next two lines, look inside the if (total.newMostToTake < 0) just above\n payload = await this.addSlices(pile);\n mostToTake = mostToTake / dcpConfig.job.uploadIncreaseFactor;\n }\n }\n\n // and finally assign whatever mostToTake was at the end of this run of the function to be returned \n // as part of the payload in case addSlices was called recursively\n payload.mostToTake = mostToTake;\n \n /* contains the job's lastSliceNumber (the only externally-meaningful value returned from \n * the uploading of slices to the scheduler) in case the calling function needs it \n */\n return payload;\n }\n\n /**\n * job.snapshot(): Private function used to populate the payloadDetails from private data,\n * inferred data, etc. Once this function has run, the payloadDetails are\n * considered authoritatively up to date until the calling function returns\n * or awaits.\n */\n [SNAPSHOT]() {\n const pd = this[INTERNAL_SYMBOL].payloadDetails;\n\n pd.type = 'ad-hoc'; /* @todo implement appliances */\n pd.uuid = this.uuid;\n pd.workFunctionURI = this[INTERNAL_SYMBOL].workFunctionURI;\n pd.dependencies = this[INTERNAL_SYMBOL].dependencies;\n pd.requirePath = this[INTERNAL_SYMBOL].requirePath;\n pd.modulePath = this[INTERNAL_SYMBOL].modulePath;\n pd.resultStorageType = this[INTERNAL_SYMBOL].resultStorageType;\n pd.resultStorageDetails = this[INTERNAL_SYMBOL].resultStorageDetails;\n pd.resultStorageParams = this[INTERNAL_SYMBOL].resultStorageParams;\n pd.force100pctCPUDensity = this[INTERNAL_SYMBOL].force100pctCPUDensity;\n\n pd.requirements = this.requirements;\n applyObjectSchema(pd.requirements, DEFAULT_REQUIREMENTS, 'Requirements Object');\n \n // @todo: 'figure this out' - wise words from eddie /mp jan 2019\n if (!pd.options) { pd.options = {}; }\n if (!pd.public) { pd.public = {}; } \n\n for (let p of ['name', 'description', 'link']) {\n if (typeof this.public[p] === 'string') {\n pd.public[p] = this.public[p];\n }\n }\n\n // The max value that the client is willing to spend to deploy\n // (list on the scheduler, doesn't include compute payment)\n /// maxDeployPayment is the max the user is willing to pay to DCP (as a\n /// Hold), in addition to the per-slice offer and associated scrape.\n /// Currently calculated as `deployCost = costPerKB *\n /// (JSON.stringify(job).length / 1024) // 1e-9 per kb`\n // @todo: figure this out / er nov 2018\n pd.maxDeployPayment = 1;\n \n /// payloadDetails.timing can be provided as an initial estimate of slice time, to\n /// give a more useful useful calculated heap value (heap.value is more or less\n /// dcc-per-millisecond)\n pd.timing = pd.timing || 1; \n }\n\n /** Escrow additional funds for this job\n * @access public\n * @param {number|BigNumber} fundsRequired - A number or BigNumber instance representing the funds to escrow for this job\n */\n async escrow (fundsRequired) {\n if ((typeof fundsRequired !== 'number' && !BigNumber.isBigNumber(fundsRequired))\n || fundsRequired <= 0 || !Number.isFinite(fundsRequired) || Number.isNaN(fundsRequired)) {\n throw new Error(`Job.escrow: fundsRequired must be a number greater than zero. (not ${fundsRequired})`);\n }\n\n const response = await this.bankConnection.send('embiggenFeeStructure', {\n feeStructureAddress: this[INTERNAL_SYMBOL].payloadDetails.feeStructureId,\n additionalEscrow: BigNumber(fundsRequired),\n fromAddress: this.paymentAccountKeystore.address,\n }, this.paymentAccountKeystore);\n\n this.receipt = response.payload;\n\n return this.receipt;\n }\n\n async _pack () {\n var retval = __webpack_require__(/*! ./node-modules */ \"./src/dcp-client/job/node-modules.js\").createModuleBundle(this[INTERNAL_SYMBOL].dependencies);\n return retval;\n }\n\n /** \n * Collect all of the dependencies together, throw them into a BravoJS\n * module which sideloads them as a side effect of declaration, and transmit\n * them to the package manager. Then we return the package descriptor object,\n * which is guaranteed to have only one file in it.\n *\n * @returns {object} with properties name and files[0]\n */\n async _publishLocalModules() {\n const { tempFile, hash, unresolved } = await this._pack();\n\n if (!tempFile) {\n return { unresolved };\n }\n\n const sideloaderFilename = tempFile.filename;\n const pkg = {\n name: `dcp-pkg-v1-localhost-${hash.toString('hex')}`,\n version: '1.0.0',\n files: {\n [sideloaderFilename]: `${sideloaderModuleIdentifier}.js`,\n },\n }\n\n await dcpPublish.publish(pkg);\n tempFile.remove();\n\n return { pkg, unresolved };\n }\n\n /**\n * Deploys the job to the scheduler.\n * @param {number | object} [slicePaymentOffer=compute.marketValue] - Amount\n * in DCC that the user is willing to pay per slice.\n * @param {Keystore} [paymentAccountKeystore=wallet.get] - An instance of the\n * Wallet API Keystore that's used as the payment account when executing the\n * job.\n * @param {object} [initialSliceProfile] - An object describing the cost the\n * user believes the average slice will incur.\n * @access public\n * @emits Job#accepted\n */\n async exec(slicePaymentOffer = __webpack_require__(/*! ../compute */ \"./src/dcp-client/compute.js\").compute.marketValue, paymentAccountKeystore, initialSliceProfile) {\n if (this[INTERNAL_SYMBOL].connected) {\n throw new Error('Exec called twice on the same job handle.');\n }\n \n if (this.estimationSlices === Infinity)\n this[INTERNAL_SYMBOL].payloadDetails.estimationSlices = null;\n else if (this.estimationSlices <= 0)\n throw new Error('Incorrect value for estimationSlices; it can be an integer or Infinity!');\n else\n this[INTERNAL_SYMBOL].payloadDetails.estimationSlices = this.estimationSlices;\n\n /* eagerly connect to depedent services for better performance */\n this.eventSubscriber.eventRouterConnection.keepalive();\n this.deployConnection.keepalive();\n\n this.readyState = 'exec'\n this.emit('readystatechange', this.readyState)\n\n if (typeof slicePaymentOffer !== 'undefined') this.setSlicePaymentOffer(slicePaymentOffer);\n if (typeof initialSliceProfile !== 'undefined') this.initialSliceProfile = initialSliceProfile;\n if (typeof paymentAccountKeystore !== 'undefined') {\n /** XXX @todo deprecate use of ethereum wallet objects */\n if (typeof paymentAccountKeystore === 'object' && paymentAccountKeystore.hasOwnProperty('_privKey')) {\n console.warn('* deprecated API * - job.exec invoked with ethereum wallet object as paymentAccountKeystore') /* /wg oct 2019 */\n paymentAccountKeystore = paymentAccountKeystore._privKey\n }\n /** XXX @todo deprecate use of private keys */\n if (wallet.isPrivateKey(paymentAccountKeystore)) {\n console.warn('* deprecated API * - job.exec invoked with private key as paymentAccountKeystore') /* /wg dec 2019 */\n paymentAccountKeystore = await new wallet.Keystore(paymentAccountKeystore, '');\n }\n\n this.setPaymentAccountKeystore(paymentAccountKeystore)\n }\n\n // Unlock\n if (this[INTERNAL_SYMBOL].paymentAccountKeystore) {\n // Throws if they fail to unlock, we allow this since the keystore was set programmatically. \n await this[INTERNAL_SYMBOL].paymentAccountKeystore.unlock(undefined, parseFloat(dcpConfig.job.maxDeployTime));\n } else {\n // If not set programmatically, we keep trying to get an unlocked keystore ... forever.\n let locked = true;\n let safety = 0; // no while loop shall go unguarded\n let ks;\n do {\n ks = null;\n // custom message for the browser modal to denote the purpose of keystore submission\n let msg = `This application is requesting a keystore file to execute ${this.public.description || this.public.name || 'this job'}. Please upload the corresponding keystore file. If you upload a keystore file which has been encrypted with a passphrase, the application will not be able to use it until it prompts for a passphrase and you enter it.`;\n try {\n ks = await wallet.get({ contextId: this.contextId, jobName: this.public.name, msg});\n } catch (e) {\n if (e.code !== ClientModal.CancelErrorCode) throw e;\n };\n if (ks) {\n try {\n await ks.unlock(undefined, parseFloat(dcpConfig.job.maxDeployTime));\n locked = false;\n } catch (e) {\n const expectedCodes = [wallet.unlockFailErrorCode, ClientModal.CancelErrorCode];\n if (!expectedCodes.includes(e.code)) throw e;\n }\n }\n if (safety++ > 1000) throw new Error('EINFINITY: job.exec tried wallet.get more than 1000 times.')\n } while (locked);\n this.setPaymentAccountKeystore(ks)\n }\n\n // We either have a valid keystore + password or we have rejected by this point.\n if (!this.slicePaymentOffer) {\n throw new Error('A payment profile must be assigned before executing the job');\n } else {\n let pd = this[INTERNAL_SYMBOL].payloadDetails;\n pd.feeStructure = this[INTERNAL_SYMBOL].slicePaymentOffer.toFeeStructure(pd.data.length);\n }\n\n if (!this.address) {\n try {\n this.readyStateChange('init');\n await this[DEPLOY_JOB]();\n // localExec jobs are not entered in any compute group.\n if (!this[INTERNAL_SYMBOL].payloadDetails.localExec) {\n // remove the public compute group from the list of compute groups in case of infinite estimationSlices(DCP-2593)\n let computeGroupIds = this.computeGroups.map(prop => prop.id);\n if (this[INTERNAL_SYMBOL].payloadDetails.estimationSlices === null && computeGroupIds.includes(1))\n this.computeGroups = this.computeGroups.filter(item => item.id !== 1);\n // Add this job to its currently-defined compute groups (as well as public group, if included)\n await computeGroups.addJobToGroups(this.address, this.computeGroups);\n \n computeGroups\n .closeServiceConnection()\n .catch((err) =>\n console.error(\n 'Warning: could not close compute groups service connection',\n err,\n ),\n );\n }\n\n this.readyState = 'deployed';\n this.emit('readystatechange', this.readyState);\n this.emit('accepted');\n } catch (error) {\n if (ON_BROWSER) {\n await ClientModal.alert(error, { title: 'Failed to deploy job!' });\n }\n\n throw error;\n }\n } else {\n await this[ADD_LISTENERS]();\n\n this.readyState = 'reconnected';\n this.emit('readystatechange', this.readyState);\n }\n \n let data = this[INTERNAL_SYMBOL].dataValues;\n \n // if job data is by value then upload data to the scheduler in a staggered fashion\n if (Array.isArray(data))\n await this.addSlices(data).then(() => {\n return this.close();\n });\n\n this[ON_STATUS](this[INTERNAL_SYMBOL].payloadDetails.status);\n this[INTERNAL_SYMBOL].connected = true;\n\n return new Promise((resolve, reject) => {\n const onComplete = () => resolve(this.results);\n const onCancel = (event) => {\n /**\n * FIXME(DCP-1150): Remove this since normal cancel event is noisy\n * enough to not need stopped event too.\n */\n if (ON_BROWSER && !this[INTERNAL_SYMBOL].listeningForCancel)\n this[INTERNAL_SYMBOL].cancelAlert(event.reason);\n this.emit('cancel', event);\n\n let errorMsg = event.reason;\n if (event.error)\n errorMsg = errorMsg +`\\n Recent error massage: ${event.error.message}`\n \n reject(new DCPError(errorMsg, event.code));\n };\n\n this[INTERNAL_SYMBOL].events.once('stopped', async (stopEvent) => {\n this.emit('stopped', stopEvent.runStatus);\n switch (stopEvent.runStatus) {\n case jobStatus.finished:\n if (this.collateResults) {\n let report = await this.getJobInfo();\n // fetch results for remain slices\n let fetchedSliceNumbers = this[INTERNAL_SYMBOL].resultsAvailable.reduce((a,e,i) => {\n if(e) a.push(i);\n return a;\n }, []);\n\n let allSliceNumbers = Array.from(Array(report.totalSlices)).map((e,i)=>i+1);\n let remainSliceNumbers = allSliceNumbers.filter( function(e) {\n return !fetchedSliceNumbers.includes(e);\n });\n\n if (remainSliceNumbers.length)\n {\n const promises = remainSliceNumbers.map(sliceNumber => this.results.fetch([sliceNumber], true));\n await Promise.all(promises);\n }\n }\n \n this.emit('complete', this.results);\n onComplete();\n break;\n case jobStatus.cancelled:\n onCancel(stopEvent);\n break;\n default:\n /**\n * Asserting that we should never be able to reach here. The only\n * scheduler events that should trigger the Job's 'stopped' event\n * are jobStatus.cancelled, jobStatus.finished, and sliceStatus.paused.\n */\n reject(\n new Error(\n `Unknown event \"${stopEvent.runStatus}\" caused the job to be stopped.`,\n ),\n );\n break;\n }\n });\n\n if (!this[INTERNAL_SYMBOL].payloadDetails.running) {\n const runStatus = this[INTERNAL_SYMBOL].payloadDetails.runStatus;\n this[INTERNAL_SYMBOL].events.emit('stopped', { runStatus });\n }\n })\n .finally(() => {\n const handleErr = (e) => {\n console.error('Error while closing job connection:');\n console.error(e);\n }\n\n // Create an async IIFE to not block the promise chain\n (async () => {\n //delay to let last few events to be received\n await new Promise((resolve) => setTimeout(resolve, 1000));\n \n // close all of the connections so that we don't cause node processes to hang.\n await this.eventSubscriber.close().catch(handleErr);\n this.deployConnection.off('close', this.openDeployConn);\n await this.deployConnection.close().catch(handleErr);\n\n this.bankConnection.off('close', this.openDeployConn)\n await this.bankConnection.close().catch(handleErr);\n \n })();\n });\n }\n\n /**\n * job.addListeners(): Private function used to set up event listeners to the scheduler\n * before deploying the job.\n */\n async [ADD_LISTENERS] () {\n // This is important: We need to flush the task queue before adding listeners\n // because we queue pending listeners by listening to the newListener event (in the constructor).\n // If we don't flush here, then the newListener events may fire after this function has run,\n // and the events won't be properly set up.\n await new Promise(resolve => setTimeout(resolve, 0));\n\n // @todo: Listen for an estimated cost, probably emit an \"estimated\" event when it comes in?\n // also @todo: Do the estimation task(s) on the scheduler and send an \"estimated\" event\n\n // Always listen to the stop event. It will resolve the work function promise, so is always needed.\n this.on('stop', (ev) => {\n this[INTERNAL_SYMBOL].events.emit('stopped', ev)\n });\n\n // Connect listeners that were set up before exec\n const evts = Array.from(this[INTERNAL_SYMBOL].subscribedEvents);\n if (evts.includes('result'))\n this.listeningForResults = true;\n this[INTERNAL_SYMBOL].subscribedEvents.clear();\n await this[LISTEN_TO_EVENTS](evts);\n\n // Connect listeners that are set up after exec\n this.on('newListener', (evt) => {\n if (evt === 'newListener') return;\n this[LISTEN_TO_EVENTS]([evt]);\n });\n \n if (this.collateResults && !this.listeningForResults) {\n // automatically add a listener for results\n this.on('result', () => {});\n }\n\n // Connect work event listeners that were set up before exec\n const workEvts = Array.from(this[INTERNAL_SYMBOL].subscribedWorkerEvents);\n this[INTERNAL_SYMBOL].subscribedWorkerEvents.clear();\n for (let evt of workEvts) {\n await this[LISTEN_TO_WORK_EVENTS](evt);\n }\n\n // Connect work event listeners that are set up after exec\n this.work.on('newListener', (evt) => {\n if (evt === 'newListener') return;\n this[LISTEN_TO_WORK_EVENTS](evt);\n });\n }\n\n /**\n * Subscribes to either reliable events or optional events\n * @param {string[]} events \n */\n async [LISTEN_TO_EVENTS](events) {\n\n const reliableEvents = [];\n const optionalEvents = [];\n for (let eventName of events) {\n eventName = eventName.toLowerCase();\n if (this[INTERNAL_SYMBOL].subscribedEvents.has(eventName))\n {\n // already subscribed to this event\n continue;\n }\n else\n {\n this[INTERNAL_SYMBOL].subscribedEvents.add(eventName);\n \n if (this.eventTypes[eventName] && this.eventTypes[eventName].reliable)\n {\n reliableEvents.push(eventName)\n }\n else if (this.eventTypes[eventName] && !this.eventTypes[eventName].reliable)\n {\n optionalEvents.push(eventName)\n }\n else\n {\n // console.debug('606: listening for unexpected/unsupported event:', eventName);\n }\n }\n }\n await this.eventSubscriber.subscribeManyEvents(reliableEvents, optionalEvents, { filter: { job: this.address } })\n }\n\n /**\n * Establishes listeners for worker events when requested by the client\n * @param {string} eventName \n */\n async [LISTEN_TO_WORK_EVENTS](eventName) {\n if (this[INTERNAL_SYMBOL].subscribedWorkerEvents.has(eventName)) {\n // already subscribed to this event\n return;\n }\n else\n {\n this[INTERNAL_SYMBOL].subscribedWorkerEvents.add(eventName);\n this.eventIntercepts.custom = (ev) => this.work.emit(eventName, ev)\n const optionalEvents = ['custom'];\n await this.eventSubscriber.subscribeManyEvents([], optionalEvents, { filter: { job: this.address } });\n }\n }\n\n /**\n * Takes result events as input, stores the result and fires off\n * events on the job handle as required. (result, duplicate-result)\n *\n * @param {object} ev - the event recieved from protocol.listen('/results/0xThisGenAdr')\n */\n async [ON_RESULT] (ev) {\n if (this[INTERNAL_SYMBOL].results === null) {\n // This should never happen - the onResult event should only be established/called\n // in addListeners which should also initialize the internal results array\n throw new Error('Job.onResult was invoked before initializing internal results');\n }\n \n const { result: _result, time } = ev.result;\n let result = await fetchURI(_result);\n\n if (this[INTERNAL_SYMBOL].results[ev.sliceNumber]) {\n const changed = JSON.stringify(this[INTERNAL_SYMBOL].results[ev.sliceNumber]) !== JSON.stringify(result);\n this.emit('duplicate-result', { sliceNumber: ev.sliceNumber, changed });\n }\n\n this[INTERNAL_SYMBOL].results[ev.sliceNumber] = result;\n this[INTERNAL_SYMBOL].resultsAvailable[ev.sliceNumber] = true;\n this.emit('result', { sliceNumber: ev.sliceNumber, result });\n this.emit('resultsUpdated');\n }\n\n /**\n * Receives status events from the scheduler, updates the local status object\n * and emits a 'status' event\n *\n * @param {object} ev - the status event received from\n * protocol.listen('/status/0xThisGenAdr')\n * @param {boolean} emitStatus - value indicating whether or not the status\n * event should be emitted\n */\n [ON_STATUS]({ runStatus, total, distributed, computed }, emitStatus = true) {\n Object.assign(this[INTERNAL_SYMBOL].status, {\n runStatus,\n total,\n distributed,\n computed,\n });\n\n if (emitStatus) {\n this.emit('status', this.status);\n }\n }\n\n /**\n * Sends a request to the scheduler to deploy the job.\n */\n async [DEPLOY_JOB] () {\n const { payloadDetails } = this[INTERNAL_SYMBOL];\n \n this[SNAPSHOT](); /* .payloadDetails now up to date */\n \n /* Send sideloader bundle to the package server */\n if (DCP_ENV.platform === 'nodejs' && this[INTERNAL_SYMBOL].dependencies.length) {\n let {pkg, unresolved} = await this._publishLocalModules();\n\n payloadDetails.dependencies = unresolved;\n if (pkg)\n payloadDetails.dependencies.push(pkg.name + '/' + sideloaderModuleIdentifier);\n }\n \n this.readyState = 'preauth';\n this.emit('readystatechange', this.readyState);\n\n /* eagerly connect to dependent services for better performance */\n computeGroups.keepAlive()\n\n const adhocId = payloadDetails.uuid.slice(payloadDetails.uuid.length - 6, payloadDetails.uuid.length);\n const schedId = await dcpConfig.scheduler.identity;\n const myId = await wallet.getId();\n const preauthToken = await bankUtil.preAuthorizePayment(schedId, payloadDetails.maxDeployPayment, this.paymentAccountKeystore);\n const { dataRange, dataValues, dataPattern, sliceCount } = marshalInputData(payloadDetails.data);\n if(dataValues)\n this[INTERNAL_SYMBOL].dataValues = dataValues;\n \n this.readyState = 'deploying';\n this.emit('readystatechange', this.readyState);\n \n /* Payload format is documented in scheduler-v4/libexec/job-submit/operations/submit.js */\n const submitPayload = {\n owner: myId.address,\n paymentAccount: this.paymentAccountKeystore.address,\n priority: 0, // @nyi\n\n workFunctionURI: payloadDetails.workFunctionURI,\n uuid: payloadDetails.uuid,\n mvMultSlicePayment: +payloadDetails.feeStructure.marketValue || 0, // @todo: improve feeStructure internals to better reflect v4\n absoluteSlicePayment: +payloadDetails.feeStructure.maxPerRequest || 0,\n requirePath: payloadDetails.requirePath,\n modulePath: payloadDetails.modulePath,\n dependencies: payloadDetails.dependencies,\n requirements: payloadDetails.requirements, /* capex */\n localExec: payloadDetails.localExec,\n force100pctCPUDensity: this.force100pctCPUDensity,\n estimationSlices: payloadDetails.estimationSlices,\n\n description: payloadDetails.public.description || 'Discreetly making the world smarter',\n name: payloadDetails.public.name || 'Ad-Hoc Job' + adhocId,\n \n preauthToken, // XXXwg/er @todo: validate this after fleshing out the stub(s)\n\n resultStorageType: payloadDetails.resultStorageType, // @todo: implement other result types\n resultStorageDetails: payloadDetails.resultStorageDetails, // Content depends on resultStorageType\n resultStorageParams: payloadDetails.resultStorageParams, // Post params for off-prem storage\n dataRange,\n dataPattern,\n sliceCount\n };\n\n /* Determine thee type of the arguments option and set the submit message payload accordingly. */\n if (Array.isArray(payloadDetails.arguments) && payloadDetails.arguments.length === 1 && payloadDetails.arguments[0] instanceof DcpURL) {\n submitPayload.arguments = payloadDetails.arguments[0].href;\n } else if (payloadDetails.arguments instanceof RemoteDataSet) {\n submitPayload.marshaledArguments = kvin.marshal(payloadDetails.arguments.map(e => new URL(e)))\n } else if (payloadDetails.arguments) {\n try {\n submitPayload.marshaledArguments = kvin.marshal(Array.from(payloadDetails.arguments));\n } catch(e) {\n throw new Error(`Could not convert job arguments to Array (${e.message})`);\n }\n }\n\n // XXXpfr Excellent tracing.\n if (debugging('dcp-client')) {\n dumpObject(submitPayload, 'Submit: Job Index: Examine submitPayload', 128);\n }\n \n // Deploy the job!\n const deployed = await this.deployConnection.send('submit', submitPayload, myId);\n\n if (!deployed.success) {\n if(deployed.payload.code === 'ENOTFOUND') {\n throw new DCPError(`Failed to submit job to scheduler. Account: ${submitPayload.paymentAccount} was not found or does not have sufficient balance (${deployed.payload.info.deployCost} DCCs needed to deploy this job)`, deployed.payload);\n } else {\n throw new DCPError('Failed to submit job to scheduler', deployed.payload);\n }\n }\n\n this.address = payloadDetails.address = deployed.payload.job;\n this[INTERNAL_SYMBOL].deployCost = deployed.payload.deployCost;\n \n if (!payloadDetails.status)\n payloadDetails.status = {\n runStatus: null,\n total: 0,\n computed: 0,\n distributed: 0,\n };\n \n payloadDetails.runStatus = payloadDetails.status.runStatus = deployed.payload.status;\n payloadDetails.status.total = deployed.payload.lastSliceNumber;\n payloadDetails.running = true;\n \n this.readyState = 'listeners';\n\n this.emit('readystatechange', this.readyState);\n\n const listenersP = this[ADD_LISTENERS]();\n\n this[INTERNAL_SYMBOL].payloadDetails = {\n ...this[INTERNAL_SYMBOL].payloadDetails,\n ...payloadDetails,\n };\n \n return listenersP;\n }\n\n /**\n * This function is identical to exec, except that the job is executed locally\n * in the client.\n * @async\n * @param {number} cores - the number of local cores in which to execute the job.\n * @param {...any} args - The remaining arguments are identical to the arguments of exec\n * @return {Promise<ResultHandle>} - resolves with the results of the job, rejects on an error\n * @access public\n */\n localExec (cores = 1, ...args) {\n this[INTERNAL_SYMBOL].payloadDetails.localExec = true;\n this[INTERNAL_SYMBOL].payloadDetails.estimationSlices = 0;\n\n let worker;\n this.on('accepted', () => {\n // Start a worker for this job\n worker = new Worker({\n localExec: true,\n jobAddresses: [this.address],\n paymentAddress: this[INTERNAL_SYMBOL].paymentAccountKeystore.address,\n maxWorkingSandboxes: cores,\n sandboxOptions: {\n ignoreNoProgress: true,\n SandboxConstructor: (DCP_ENV.platform === 'nodejs' &&\n __webpack_require__(/*! ../worker/evaluators */ \"./src/dcp-client/worker/evaluators/index.js\").nodeEvaluatorFactory())\n },\n });\n\n worker.start().catch((e) => {\n console.error(\"Failed to start worker for localExec:\");\n console.error(e.message);\n });\n });\n\n return this.exec(...args).finally(() => {\n if (worker) {\n setTimeout(() => {\n // stop the worker\n worker.stop(true);\n }, 3000);\n }\n });\n }\n\n /**\n * The current job status. Will have undefined values when the handle hasn't had exec called on it yet.\n * @access public\n * @readonly\n * @type {object}\n * @property {number} total Total number of slices in the job\n * @property {number} distributed Number of slices that have been distributed\n * @property {number} computed Number of slices that have completed execution (returned a result)\n * @property {string} runStatus Current runStatus of the job\n */\n get status () {\n // Shallow-copy to prevent modification\n return { ...this[INTERNAL_SYMBOL].status };\n }\n\n get requirePath() {\n return this[INTERNAL_SYMBOL].requirePath;\n }\n\n /**\n * This function specifies a module dependency (when the argument is a string)\n * or a list of dependencies (when the argument is an array) of the work\n * function. This function can be invoked multiple times before deployment.\n * @param {string | string[]} modulePaths - A string or array describing one\n * or more dependencies of the job.\n * @access public\n */\n requires(modulePaths) {\n if (\n typeof modulePaths !== 'string' &&\n (!Array.isArray(modulePaths) ||\n modulePaths.some((modulePath) => typeof modulePath !== 'string'))\n ) {\n throw new TypeError(\n 'The argument to dependencies is not a string or an array of strings',\n );\n } else if (modulePaths.length === 0) {\n throw new RangeError(\n 'The argument to dependencies cannot be an empty string or array',\n );\n } else if (\n Array.isArray(modulePaths) &&\n modulePaths.some((modulePath) => modulePath.length === 0)\n ) {\n throw new RangeError(\n 'The argument to dependencies cannot be an array containing an empty string',\n );\n }\n\n if (!Array.isArray(modulePaths)) {\n modulePaths = [modulePaths];\n }\n\n for (const modulePath of modulePaths) {\n if (modulePath[0] !== '.' && modulePath.indexOf('/') !== -1) {\n const modulePrefixRegEx = /^(.*)\\/.*?$/;\n const [, modulePrefix] = modulePath.match(modulePrefixRegEx);\n if (\n modulePrefix &&\n this[INTERNAL_SYMBOL].requirePath.indexOf(modulePrefix) === -1\n ) {\n this[INTERNAL_SYMBOL].requirePath.push(modulePrefix);\n }\n }\n\n this[INTERNAL_SYMBOL].dependencies.push(modulePath);\n }\n }\n\n get slicePaymentOffer () {\n return this[INTERNAL_SYMBOL].slicePaymentOffer;\n }\n\n /**\n * The keystore that will be used to pay for the job. Can be set with {@link Job.setPaymentAccountKeystore} or by providing a keystore to {@link Job.exec}.\n * @readonly\n * @access public\n * @type {module:dcp/wallet.AuthKeystore}\n */\n get paymentAccountKeystore () {\n return this[INTERNAL_SYMBOL].paymentAccountKeystore;\n }\n\n /** Set the account upon which funds will be drawn to pay for the job.\n * @param {module:dcp/wallet.AuthKeystore} keystore A keystore that representa a bank account.\n * @access public\n */\n setPaymentAccountKeystore (keystore) {\n if (this.address) {\n if (!keystore.address.eq(this[INTERNAL_SYMBOL].payloadDetails.owner)) {\n let message = \"Cannot change payment account after job has been deployed\";\n this.emit('EPERM', message);\n throw new Error(`EPERM: ${message}`);\n }\n }\n \n if (!(keystore instanceof wallet.Keystore)) {\n let e = new Error('Not an instance of Keystore: ' + keystore.toString())\n console.log(`Not an instance of Keystore: ${keystore}`)\n throw e\n }\n this[INTERNAL_SYMBOL].paymentAccountKeystore = keystore;\n }\n\n /** Set the slice payment offer. This is equivalent to the first argument to exec.\n * @param {number} slicePaymentOffer - The number of DCC the user is willing to pay to compute one slice of this job\n */\n setSlicePaymentOffer (slicePaymentOffer) {\n this[INTERNAL_SYMBOL].slicePaymentOffer = new SlicePaymentOffer(slicePaymentOffer);\n }\n\n /**\n * @param {URL} locationUrl - A URL object \n * @param {object} postParams - An object with any parameters that a user would like to be passed to a \n * remote result location. This object is capable of carry API keys for S3, \n * DropBox, etc. These parameters are passed as parameters in an \n * application/x-www-form-urlencoded request.\n */\n setResultStorage(locationUrl, postParams) {\n if (locationUrl instanceof URL || locationUrl instanceof DcpURL) {\n this[INTERNAL_SYMBOL].resultStorageDetails = locationUrl;\n } else {\n const e = new Error('Not an instance of a DCP URL: ' + locationUrl);\n console.log('Not an instance of a DCP URL ' + locationUrl);\n throw e;\n }\n\n // resultStorageParams contains any post params required for off-prem storage\n if (typeof postParams !== 'undefined' && typeof postParams === 'object' ) {\n this[INTERNAL_SYMBOL].resultStorageParams = postParams;\n } else {\n const e = new Error('Not an instance of a object: ' + postParams);\n console.log('Not an instance of an object ' + postParams);\n throw e;\n } \n\n // Some type of object here\n this[INTERNAL_SYMBOL].resultStorageType = 'pattern';\n }\n\n /** close an open job to indicate we are done adding data so it is okay to finish\n * the job at the appropriate time\n */\n async close() {\n return this.deployConnection.send('closeJob', {\n job: this.id,\n });\n }\n}\n\n/**\n * @typedef {object} MarshaledInputData\n * @property {any[]} [dataValues]\n * @property {string} [dataPattern]\n * @property {RangeObject} [dataRange]\n * @property {number} [sliceCount]\n */\n\n/**\n * Depending on the shape of the job's data, resolve it into a RangeObject, a\n * Pattern, or a values array, and return it in the appropriate property.\n *\n * @param {any} data Job's input data\n * @return {MarshaledInputData} An object with one of the following properties set:\n * - dataValues: job input is an array of arbitrary values \n * - dataPattern: job input is a URI pattern \n * - dataRange: job input is a RangeObject (and/or friends)\n */\nfunction marshalInputData(data) {\n if (!(data instanceof Object\n || data instanceof SuperRangeObject)) {\n throw new TypeError(`Invalid job data type: ${typeof data}`);\n }\n\n /**\n * @type MarshaledInputData\n */\n const marshalledInputData = {};\n\n // TODO(wesgarland): Make this more robust.\n if (data instanceof SuperRangeObject ||\n (data.hasOwnProperty('ranges') && data.ranges instanceof MultiRangeObject) ||\n (data.hasOwnProperty('start') && data.hasOwnProperty('end'))) {\n marshalledInputData.dataRange = data;\n } else if (Array.isArray(data)) {\n marshalledInputData.dataValues = data;\n } else if (data instanceof URL || data instanceof DcpURL) {\n marshalledInputData.dataPattern = String(data);\n } else if(data instanceof RemoteDataSet) {\n marshalledInputData.dataValues = data.map(e => new URL(e));\n } else if(data instanceof RemoteDataPattern) {\n marshalledInputData.dataPattern = data['pattern'];\n marshalledInputData.sliceCount = data['sliceCount'];\n }\n\n log('marshalledInputData:', marshalledInputData);\n return marshalledInputData;\n}\n\nObject.assign(exports, {\n Job,\n SlicePaymentOffer,\n ResultHandle,\n});\n\n\n//# sourceURL=webpack:///./src/dcp-client/job/index.js?");
|
|
5085
|
+
eval("/**\n * @file job/index.js\n * @author Eddie Roosenmaallen, eddie@kingsds.network\n * Matthew Palma, mpalma@kingsds.network\n * @date November 2018\n *\n * This module implements the Compute API's Job Handle\n *\n */\n\n/** @typedef {import('dcp/dcp-client/wallet/keystore').Keystore} Keystore */\n\n\nconst BigNumber = __webpack_require__(/*! bignumber.js */ \"./node_modules/bignumber.js/bignumber.js\");\nconst { v4: uuidv4 } = __webpack_require__(/*! uuid */ \"./node_modules/uuid/dist/esm-browser/index.js\");\nconst { EventEmitter, PropagatingEventEmitter } = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\");\nconst { RangeObject, MultiRangeObject, DistributionRange, SuperRangeObject } = __webpack_require__(/*! dcp/dcp-client/range-object */ \"./src/dcp-client/range-object.js\");\nconst { fetchURI, encodeDataURI, dumpObject } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\nconst { getTextEncoder } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\nconst serialize = __webpack_require__(/*! dcp/utils/serialize */ \"./src/utils/serialize.js\");\nconst wallet = __webpack_require__(/*! dcp/dcp-client/wallet */ \"./src/dcp-client/wallet/index.js\");\nconst protocolV4 = __webpack_require__(/*! dcp/protocol-v4 */ \"./src/protocol-v4/index.js\");\nconst { EventSubscriber } = __webpack_require__(/*! dcp/events/event-subscriber */ \"./src/events/event-subscriber.js\");\nconst { DcpURL } = __webpack_require__(/*! dcp/common/dcp-url */ \"./src/common/dcp-url.js\");\nconst ClientModal = __webpack_require__(/*! dcp/dcp-client/client-modal */ \"./src/dcp-client/client-modal/index.js\");\nconst { Worker } = __webpack_require__(/*! dcp/dcp-client/worker */ \"./src/dcp-client/worker/index.js\");\nconst { RemoteDataSet } = __webpack_require__(/*! dcp/dcp-client/remote-data-set */ \"./src/dcp-client/remote-data-set.js\");\nconst { RemoteDataPattern } = __webpack_require__(/*! dcp/dcp-client/remote-data-pattern */ \"./src/dcp-client/remote-data-pattern.js\");\nconst { ResultHandle } = __webpack_require__(/*! ./result-handle */ \"./src/dcp-client/job/result-handle.js\");\nconst { SlicePaymentOffer } = __webpack_require__(/*! ./slice-payment-offer */ \"./src/dcp-client/job/slice-payment-offer.js\");\nconst DCP_ENV = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\nconst dcpPublish = __webpack_require__(/*! dcp/common/dcp-publish */ \"./src/common/dcp-publish.js\");\nconst computeGroups = __webpack_require__(/*! dcp/dcp-client/compute-groups */ \"./src/dcp-client/compute-groups/index.js\");\nconst schedulerConstants = __webpack_require__(/*! dcp/common/scheduler-constants */ \"./src/common/scheduler-constants.js\");\nconst { 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\");\n\nconst TextEncoder = getTextEncoder();\nlet dannyDebugCounter = 0;\n\nconst log = (...args) => {\n if (debugging('job')) {\n console.debug('dcp-client:job', ...args);\n }\n};\n\nconst ON_BROWSER = DCP_ENV.isBrowserPlatform;\nconst sideloaderModuleIdentifier = 'sideloader-v1';\n\n// Symbols used to hide private members and functions on the Job instance\nconst debugBuild = (__webpack_require__(/*! dcp/common/dcp-build */ \"./src/common/dcp-build.js\").build === 'debug');\nconst INTERNAL_SYMBOL = debugBuild ? '_' : Symbol('Job Internals');\nconst SNAPSHOT = debugBuild ? '_snapshot' : Symbol('Job.snapshot');\nconst DEPLOY_JOB = debugBuild ? '_deploy' : Symbol('Job.deploy');\n\nconst ADD_LISTENERS = Symbol('Job.addListeners');\nconst LISTEN_TO_EVENTS = Symbol('Job.listenToEvents');\nconst LISTEN_TO_WORK_EVENTS = Symbol('Job.listenToWorkEvents');\nconst ON_RESULT = Symbol('Job.onResult');\nconst ON_STATUS = Symbol('Job.onStatus');\n\nexports.JOB_INTERNAL_SYMBOL = INTERNAL_SYMBOL; /* allow friends to access our guts, eg. job/result-handle */\n\nconst DEFAULT_REQUIREMENTS = {\n engine: {\n es7: null,\n spidermonkey: null\n },\n environment: {\n webgpu: null,\n offscreenCanvas: null,\n fdlibm: null\n },\n browser: {\n chrome: null\n },\n details: {\n offscreenCanvas: {\n bigTexture4096: null,\n bigTexture8192: null,\n bigTexture16384: null,\n bigTexture32768: null,\n }\n },\n discrete: null,\n};\nconst ZERO_COST_HOLD_ADDRESS = '0x' + '0'.repeat(130);\n\n/** @typedef {import('../range-object').RangeLike} RangeLike */\n\n/**\n * Ensure input data is an appropriate format\n * @param {RangeObject | DistributionRange | RemoteDataSet | Array | Iterable}\n * inputData - A URI-shaped string, a [Multi]RangeObject-constructing value, or\n * an array of slice data\n * @return {RangeObject | RangeLike | DistributionRange | RemoteDataSet | Array}\n * The coerced input in an appropriate format ([Multi]RangeObject,\n * DistributionRange, RemoteDataSet, or array)\n */\nconst wrangleData = (inputData) => {\n if (typeof inputData === 'object' && !!inputData.ranges) { return new MultiRangeObject(inputData) }\n\n if (RangeObject.isRangelike(inputData)) { return inputData }\n if (RangeObject.isRangeObject(inputData)) { return inputData }\n if (DistributionRange.isDistribution(inputData)) { return inputData }\n if (RangeObject.isProtoRangelike(inputData)) { return new RangeObject(inputData) }\n if (DistributionRange.isProtoDistribution(inputData)) { return new DistributionRange(inputData) }\n if (RemoteDataSet.isRemoteDataSet(inputData)) { return inputData }\n if (RemoteDataPattern.isRemoteDataPattern(inputData)) { return inputData }\n\n return Array.isArray(inputData) ? inputData : [inputData];\n};\n\n// Used to validate the requirements object,\n// applies the default requirements schema\nconst applyObjectSchema = (obj, schema, errContext='', scope='') => {\n let checkedObjs = [];\n\n for (let p in schema) {\n let fullPropScope = scope.concat(p);\n if (!(p in obj)) {\n if (typeof schema[p] === 'object' && schema[p] !== null) {\n obj[p] = {};\n checkedObjs.push(fullPropScope);\n applyObjectSchema(obj[p], schema[p], errContext, fullPropScope.concat('.'));\n } else obj[p] = schema[p];\n } else if (typeof schema[p] === 'object' && schema[p] !== null && !checkedObjs.includes(fullPropScope)) {\n if (typeof obj[p] !== 'object') throw new Error(`${errContext}: Schema mismatch, property '${fullPropScope}' should be an object.`);\n else {\n checkedObjs.push(fullPropScope);\n applyObjectSchema(obj[p], schema[p], errContext, fullPropScope.concat('.'));\n }\n } else if ((typeof schema[p] !== 'object' || schema[p] === null)\n && typeof obj[p] !== 'boolean' && obj[p] !== null) {\n throw new Error(`${errContext}: Schema mismatch, object property '${fullPropScope}' should be a boolean.`);\n }\n }\n\n for (let p in obj) {\n let fullPropScope = scope.concat(p);\n if (!(p in schema)) throw new Error(`${errContext}: Schema mismatch, object has extra key '${fullPropScope}'.`);\n else if (typeof obj[p] === 'object' && obj[p] !== null && !checkedObjs.includes(fullPropScope)) {\n checkedObjs.push(fullPropScope);\n applyObjectSchema(obj[p], schema[p], errContext, fullPropScope.concat('.'));\n }\n }\n}\n\n/**\n * @classdesc The Compute API's Job Handle (see {@link https://docs.dcp.dev/specs/compute-api.html#job-handles|Compute API spec})\n * Job handles are objects which correspond to jobs. \n * They are created by some exports of the compute module, such as {@link module:dcp/compute.do|compute.do} and {@link module:dcp/compute.for|compute.for}.\n * @extends module:dcp/dcp-events.PropagatingEventEmitter\n * @hideconstructor\n * @access public\n */\nclass Job extends PropagatingEventEmitter {\n /**\n * This event is emitted when the job is accepted by the scheduler on deploy.\n * \n * @event Job#accepted\n * @access public\n * @type {object}\n * @property {object} job Original object that was delivered to the scheduler for deployment\n *//**\n * Fired when the job is cancelled.\n * \n * @event Job#cancel\n * @access public\n *//**\n * Fired when a result is returned.\n * \n * @event Job#result\n * @access public\n * @type {object}\n * @property {string} jobAddress Address of the job\n * @property {string} task ID of the task (slice) the result came from\n * @property {number} sort The index of the slice\n * @property {object} result\n * @property {string} result.request\n * @property {*} result.result The value returned from the work function\n *//**\n * Fired when the result handle is modified, either when a new `result` event is fired or when the results are populated with `results.fetch()`\n * \n * @event Job#resultsUpdated\n * @access public\n *//**\n * Fired when the job has been completed.\n * \n * @event Job#complete\n * @access public\n * @type {ResultHandle}\n *//**\n * Fired when the job's status changes.\n * \n * @event Job#status\n * @access public\n * @type {object}\n * @property {string} jobAddress Address of the job\n * @property {number} total Total number of slices in the job\n * @property {number} distributed Number of slices that have been distributed\n * @property {number} computed Number of slices that have completed execution (returned a result)\n * @property {string} runStatus Current runStatus of the job\n *//**\n * Fired when a slice throws an error.\n * \n * @event Job#error\n * @access public\n * @type {object}\n * @property {string} jobAddress Address of the job\n * @property {number} sliceIndex Index of the slice that threw the error\n * @property {string} message The error message\n * @property {string} stack The error stacktrace\n * @property {string} name The error type name\n *//**\n * Fired when a slice uses one of the console log functions.\n * \n * @event Job#console\n * @access public\n * @type {object}\n * @property {string} jobAddress Address of the job\n * @property {number} sliceIndex The index of the slice that produced this event\n * @property {string} level The log level, one of `debug`, `info`, `log`, `warn`, or `error`\n * @property {string} message The console log message\n *//**\n * Fired when a slice is stopped for not calling progress. Contains information about how long the slice ran for, and about the last reported progress calls.\n * \n * @event Job#noProgress\n * @access public\n * @type {object}\n * @property {string} jobAddress Address of the job\n * @property {number} sliceIndex The index of the slice that failed due to no progress\n * @property {number} timestamp How long the slice ran before failing\n * @property {object} progressReports\n * @property {object} progressReports.last The last progress report received from the worker\n * @property {number} progressReports.last.timestamp Time since the start of the slice\n * @property {number} progressReports.last.progress Progress value reported\n * @property {*} progressReports.last.value The last value that was passed to the progress function\n * @property {number} progressReports.last.throttledReports Number of calls to progress that were throttled since the last report\n * @property {object} progressReports.lastUpdate The last determinate (update to the progress param) progress report received from the worker\n * @property {number} progressReports.lastUpdate.timestamp\n * @property {number} progressReports.lastUpdate.progress\n * @property {*} progressReports.lastUpdate.value\n * @property {number} progressReports.lastUpdate.throttledReports\n *//**\n * Identical to `noProgress`, except that it also contains the data that the slice was executed with.\n * \n * @event Job#noProgressData\n * @access public\n * @type {object}\n * @property {*} data The data that the slice was executed with\n *//**\n * Fired when the job is paused due to running out of funds. The job can be resumed by escrowing more funds then resuming the job.\n * \n * Event payload is the estimated funds required to complete the job\n * \n * @event Job#ENOFUNDS\n * @access public\n * @type {BigNumber}\n *//**\n * Fired when the job is cancelled due to the work function not calling the `progress` method frequently enough.\n * \n * @event Job#ENOPROGRESS\n * @access public\n *//**\n * The job was cancelled because scheduler has determined that individual tasks in this job exceed the maximum allowable execution time.\n * \n * @event Job#ESLICETOOSLOW\n * @access public\n *//**\n * Fired when the job is cancelled because too many work functions are terminating with uncaught exceptions.\n * \n * @event Job#ETOOMANYERRORS\n * @access public\n */\n\n /**\n * @form1 new Job(job_shaped_object)\n * @form2 new Job('application_worker_address'[, data[, arguments]])\n * @form3b new Job('worker source'[, data[, arguments]])\n * @form3b new Job(worker_function[, data[, arguments]])\n */\n\n constructor() {\n super('Job');\n\n this.readyStateChange = (readyState) => {\n this.readyState = readyState;\n this.emit('readyStateChange', this.readyState);\n };\n this.readyStateChange(sliceStatus.new);\n \n /*\n * Private members\n */\n this[INTERNAL_SYMBOL] = {\n events: new EventEmitter('Job Internal'),\n connected: false, // set to true after first call to exec\n /**\n * This object holds details for generating DCPv4 messages about this job.\n * It is updated everytime we call SNAPSHOT.\n */\n payloadDetails: {\n localExec: false,\n },\n\n /**\n * The slicePaymentOffer default value is set to compute.marketValue, in .exec() \n */\n slicePaymentOffer: null,\n paymentAccountKeystore: null,\n\n /**\n * These are private but getters are provided so they can be modified but\n * not replaced.\n */\n /**\n * List of module prefixes using in CommonJS module resolution.\n * @type {string[]}\n */\n requirePath: [],\n\n /**\n * List of modules the job needs.\n * @type {string[]}\n */\n\n dependencies: [],\n\n // This array contains the names of worker events that\n // had listeners registered before exec is called, once\n // the job has been deployed then the proper event handlers\n // will be generated from this list\n subscribedEvents: new Set(),\n subscribedWorkerEvents: new Set(),\n\n results: [],\n resultsAvailable: [],\n resultStorageType: 'values',\n resultStorageDetails: undefined,\n resultStorageParams: undefined, //Holds the POST params and URL for off-prem storage\n\n // Tracks job progress\n status: {\n runStatus: null,\n total: null,\n distributed: null,\n computed: null,\n },\n\n // Cancel is special. We need to fire an `alert` when the job is canceled. \n // If they are listening for the (reliable) event then they need to be able to\n // prevent it. If not, then it'll be handled by the `exec` rejection via the 'stopped'\n // event. The result is that we want only one of two ways the `alert` can be fired\n // to be active based on whether or not the user is listening for cancel. \n // Once DCP-1150 lands, we won't need to listen on stopped since more failures will fire a cancel event.\n listeningForCancel: false,\n // TODO - cancel events should have more info in them. DCP-1150\n cancelAlert: () => ClientModal.alert(\"More details in console...\", {title: 'Job Canceled'}),\n\n listeningForError: false,\n errorAlert: (err) => ClientModal.alert(err, {title: 'Unexpected Error'}),\n\n listeningForNoFunds: false,\n noFundsAlert: (event) => {\n let msg = `Job \"${event.name}\" is paused due to insufficient funds. ${event.fundsRequired} DCC is required to compute remaining ${event.remainingSlices} slices.\\njobId: ${event.job}\\nbankAccount: ${event.bankAccount}`; \n ClientModal.alert(msg, { title: 'Job paused' })\n },\n };\n\n /*\n * Public members\n */\n // Deep copy the default requirements. I know, I hate it too\n /**\n * An object describing the requirements that workers must have to be eligible for this job. See\n * {@link https://docs.dcp.dev/specs/compute-api.html#requirements-objects|Requirements Objects}.\n *\n * @type {object}\n * @access public\n */\n this.requirements = JSON.parse(JSON.stringify(DEFAULT_REQUIREMENTS));\n this.schedulerURL = undefined;\n this.bankURL = undefined;\n this.deployURL = '';\n this.collateResults = true;\n this.listeningForResults = false;\n /**\n * @see {@link https://kingsds.atlassian.net/browse/DCP-1475?atlOrigin=eyJpIjoiNzg3NmEzOWE0OWI4NGZkNmI5NjU0MWNmZGY2OTYzZDUiLCJwIjoiaiJ9|Jira Issue}\n */\n let uuid = uuidv4();\n\n /**\n * An object describing the cost the user believes each the average slice will incur, in terms of CPU/GPU and I/O.\n * If defined, this object is used to provide initial scheduling hints and to calculate escrow amounts.\n *\n * @type {object}\n * @access public\n */\n this.initialSliceProfile = undefined;\n\n /**\n * A place to store public-facing attributes of the job. Anything stored on this object will be available inside the work \n * function (see {@link module:dcp/compute~sandboxEnv.work}). The properties documented here may be used by workers to display what jobs are currently being \n * worked on.\n * @access public\n * @property {string} name Public-facing name of this job.\n * @property {string} description Public-facing description for this job.\n * @property {string} link Public-facing link to external resource about this job.\n */\n this.public = {\n name: null,\n description: null,\n link: null,\n };\n\n this.contextId = null;\n this.force100pctCPUDensity = false;\n this.workerConsole = false;\n\n // The following public members are only populated after the job has been deployed\n this.address = null;\n this.receipt = null;\n this.meanSliceProfile = null;\n \n /* \n * A number (can be null, undefined, or infinity) describing the estimationSlicesRemaining in the jpd (dcp-2593)\n * @type {number}\n * @access public\n */\n this.estimationSlices = undefined;\n\n /* We avoid using job.id internally because it is easy to confuse with db::jobs.id, but is an API\n * interface that we present to end-user developers so we need to keep it.\n */\n Object.defineProperty(this, 'id', {\n get: () => this.address,\n set: (id) => this.address = id\n });\n \n // \n /**\n * An EventEmitter for custom events dispatched by the work function.\n * @type {module:dcp/dcp-events.EventEmitter}\n * @access public\n * @example\n * // in sandbox\n * work.emit('myEventName', 1, [2], \"three\");\n * // clientside\n * job.work.on('myEventName', (num, arr, string) => { });\n */\n this.work = new EventEmitter('job.work');\n\n //Initialize the eventSubscriber so each job has unique eventSubscriber\n this.eventSubscriber = new EventSubscriber(this);\n \n // Some events can't be emitted 'naturally' without having weird/wrong output.\n // An example of this is results. When results are returned from the scheduler,\n // They come in as a dataURI of kvin-ified results. We need to parse all that before\n // We actually send it to the client. For such events, we will intercept them, parse\n // them as needed, then emit the event with the 'fixed' data to the client.\n \n const ceci = this\n const parseConsole = function deserializeConsoleMessage(ev) {\n ceci.emit('console', ev);\n }\n \n this.eventIntercepts = {\n result: (ev) => this[ON_RESULT](ev),\n status: (ev) => this[ON_STATUS](ev),\n cancel: (ev) => this[INTERNAL_SYMBOL].events.emit('stopped', ev),\n console: parseConsole,\n }\n\n this.eventTypes = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\").eventTypes;\n\n this.work.on('newListener', (evt) => {\n if (!this[INTERNAL_SYMBOL].connected && evt !== 'newListener') {\n this[INTERNAL_SYMBOL].subscribedWorkerEvents.add(evt);\n }\n });\n\n this.on('newListener', (evt) => {\n if (!this[INTERNAL_SYMBOL].connected && evt !== 'newListener') {\n this[INTERNAL_SYMBOL].subscribedEvents.add(evt);\n }\n });\n // Form1: If arguments[0] is an object that looks like a job, this is a 'copy constructor'\n // where we inherit as much as possible from the original instance.\n if (typeof arguments[0] === 'object' &&\n arguments[0].type &&\n arguments[0].data &&\n arguments[0].public) {\n \n let src = arguments[0];\n\n this[INTERNAL_SYMBOL].payloadDetails = {\n ...src,\n data: wrangleData(src.data), // rehydrate ranges\n };\n\n if (src.feeStructure) {\n this.setSlicePaymentOffer(src.feeStructure);\n }\n \n if (src.address)\n this.address = src.address;\n if (src.payloadData.status)\n this[ON_STATUS](this[INTERNAL_SYMBOL].payload.status, false);\n if (src.public)\n Object.assign(this.public, src.public);\n } else {\n /* Forms 2-4 */ \n if (typeof arguments[0] === 'function')\n arguments[0] = arguments[0].toString();\n\n if (typeof arguments[0] === 'string') {\n this[INTERNAL_SYMBOL].workFunctionURI = encodeDataURI(arguments[0], 'application/javascript');\n } else if (DcpURL.isURL(arguments[0])) {\n this[INTERNAL_SYMBOL].workFunctionURI = arguments[0].href;\n } \n\n const wrangledInputData = wrangleData(arguments[1] || []);\n const wrangledArguments = wrangleData(arguments[2] || []);\n \n log('wrangledInputData:', wrangledInputData);\n log('wrangledArguments:', wrangledArguments);\n \n Object.assign(this[INTERNAL_SYMBOL].payloadDetails, {\n request: 'main',\n data: wrangledInputData,\n arguments: wrangledArguments,\n });\n }\n\n // This should happen last, it depends on the this.[INTERNAL_SYMBOL].payloadDetails.data array\n /**\n * A Result Handle object used to query and manipulate the output set. \n * Present once job has been deployed.\n * @type {ResultHandle}\n * @access public\n */\n this.results = new ResultHandle(this);\n\n /**\n * Read-only copy of the job's uuid (generated or rehydrated via form1 constructor)\n */\n Object.defineProperty(this, 'uuid', {\n get: () => uuid,\n configurable: false,\n enumerable: true,\n });\n \n // each entry contains the computeGroupID, joinKey, joinSecret, joinKeystore\n // Add to public compute group by default\n this.computeGroups = [ Object.assign({}, schedulerConstants.computeGroups.public) ];\n\n\n // Initialize to null so these properties are recognized for the Job class\n this.bankConnection = null;\n this.deployConnection = null;\n this.openBankConn = function openBankConn()\n {\n ceci.bankConnection = new protocolV4.Connection(dcpConfig.bank.services.bankTeller);\n ceci.bankConnection.on('close', ceci.openBankConn);\n }\n\n this.openDeployConn = function openDeployConn()\n {\n ceci.deployConnection = new protocolV4.Connection(dcpConfig.scheduler.services.jobSubmit);\n ceci.deployConnection.on('close', ceci.openDeployConn);\n }\n\n this.openBankConn();\n this.openDeployConn();\n }\n\n /** \n * Cancel the job\n * @access public\n * @param {string} reason If provided, will be sent to client\n */\n async cancel (reason = undefined) {\n const response = await this.deployConnection.send('cancelJob', {\n job: this.address,\n owner: this.paymentAccountKeystore.address,\n reason,\n }, this.paymentAccountKeystore);\n\n return response.payload;\n }\n\n /** \n * Resume this job\n * @access public\n */\n async resume() {\n const response = await this.schedulerConnection.send('resumeJob', {\n job: this.address,\n owner: this.paymentAccountKeystore.address,\n }, this.paymentAccountKeystore);\n\n return response.payload;\n }\n\n /**\n * Helper function for retrieving info about the job. The job must have already been deployed.\n * An alias for {@link module:dcp/compute.getJobInfo}.\n * @access public\n */\n async getJobInfo(){\n return await __webpack_require__(/*! ../compute */ \"./src/dcp-client/compute.js\").compute.getJobInfo(this.address);\n }\n\n /**\n * Helper function for retrieving info about the job's slices. The job must have already been deployed.\n * An alias for {@link module:dcp/compute.getSliceInfo}.\n * @access public\n */\n async getSliceInfo(){\n return await __webpack_require__(/*! ../compute */ \"./src/dcp-client/compute.js\").compute.getSliceInfo(this.address);\n }\n \n /**\n * Helper function that tries to upload slicePile to scheduler for the job with the given address\n * If the connection throws, we will continue trying to upload until it has thrown errorTolerance times\n * However, if the upload is unsuccessful, we throw immediately.\n * @param {Array} slicePile \n * @returns payload containing success property (pertaining to success of adding slices to job) as well as lastSliceNumber of job \n */\n async safeSliceUpload(slicePile)\n {\n let payload = undefined; // future return value\n let errorTolerance = dcpConfig.job.sliceUploadErrorTolerance; // copy number of times we will tolerate non-success when uploading slices directly from config\n await this.deployConnection.keepalive();\n while (true) // eslint-disable-line no-constant-condition\n {\n try\n {\n payload = await this.deployConnection.send('addSliceData', {\n job: this.address,\n dataValues: kvin.marshal(slicePile),\n });\n if (!payload.success)\n throw new DCPError('Cannot upload slice data to scheduler','EUPLOADSCHED');\n else\n break;\n }\n catch (error)\n {\n if (--errorTolerance <= 0)\n throw error;\n }\n }\n return payload;\n }\n \n /**\n * This function contains the actual logic behind staggered slice uploads\n * to the scheduler which makes quicker deployment possible.\n * \n * Note that we pass in mostToTake so that the uploadLogic function can update \n * it to the new value it needs to be, and then pass it back to the wrapper \n * function (addSlices) which actually does the work of picking up slices \n * and thus uses this value\n * @param {Array} pile the actual array of slices being uploaded to scheduler\n * @param {Number} mostToTake number of slices that should be taken by the wrapper function (addSlices) \n * which actually does the work of picking up slices and thus uses this value.\n * We pass in mostToTake so that the uploadLogic function can update it to the \n * new value it needs to be, and then pass it back to the wrapper\n * @returns payload containing success property (pertaining to success of adding slices to job) as well as lastSliceNumber of job\n */\n async sliceUploadLogic(pile, mostToTake)\n {\n const slicesTaken = pile.length;\n \n let pileSize = 0; // total size of the pile's slices in bytes\n \n // calculate pileSize by finding sum of bytesizes of each slice (after each is marshalled)\n for (let i = 0; i < slicesTaken; ++i)\n {\n let sliceSize = (new TextEncoder()).encode(kvin.stringify(pile[i])).length;\n pileSize += sliceSize;\n }\n\n let newMostToTake;\n let uploadedSlices;\n \n // if the pile is larger than the ceiling but we only took one slice, there's no smaller pile we can make\n // so we upload it anyway but we don't try taking more next time cause we were over the ceiling (which \n // is a hard limit on upload sizes)\n if ((pileSize > dcpConfig.job.uploadSlicesCeiling) && (slicesTaken === 1))\n {\n uploadedSlices = await this.safeSliceUpload(pile);\n newMostToTake = 1;\n }\n \n // if the pile is larger than the target but we only took one slice, there's no smaller pile we can make\n // so we upload it anyway and still try taking more\n else if ((pileSize > dcpConfig.job.uploadSlicesTarget) && (slicesTaken === 1))\n {\n uploadedSlices = await this.safeSliceUpload(pile);\n newMostToTake = mostToTake * dcpConfig.job.uploadIncreaseFactor;\n }\n \n // otherwise, if the pile is smaller than the soft ceiling, send up the pile anyway (since piles are expensive to make) \n // but remember to include incrementFactor times as many slices in the next pile\n else if (pileSize <= dcpConfig.job.uploadSlicesTarget)\n {\n uploadedSlices = await this.safeSliceUpload(pile);\n newMostToTake = mostToTake * dcpConfig.job.uploadIncreaseFactor;\n }\n \n // if the pile is over the ceiling then we do not upload and begin reassembling our piles from scratch\n else if (pileSize > dcpConfig.job.uploadSlicesCeiling)\n {\n newMostToTake = -1;\n }\n \n // if the pile is over the target (but implicitly under the ceiling), then upload the pile to scheduler but lower mostToTake\n // by a smaller factor than incrementFactor to allow us to begin \"centering\" sizes of piles around the target\n else if (pileSize > dcpConfig.job.uploadSlicesTarget)\n {\n uploadedSlices = await this.safeSliceUpload(pile);\n newMostToTake = Math.ceil(mostToTake / ((2 / 3) * dcpConfig.job.uploadIncreaseFactor));\n }\n\n if (uploadedSlices && uploadedSlices.success && typeof uploadedSlices.payload.lastSliceNumber !== 'undefined')\n // must check if uploadedSlices exists first since if pileSize > ceiling then there will be no uploadedSlices\n this.status.total = uploadedSlices.payload.lastSliceNumber;\n\n let payload = uploadedSlices ? uploadedSlices.payload : undefined;\n return { payload, newMostToTake }; // in case the user needs lastSliceNumber's value\n }\n \n /**\n * Uploads slices to the scheduler in a staggered fashion\n * @param {Array} dataValues actual array of slices being uploaded to scheduler\n * @returns payload containing success property (pertaining to success of adding slices to job) as well as lastSliceNumber of job\n */\n async addSlices(dataValues)\n {\n if (!Array.isArray(dataValues))\n throw new TypeError('Only data-by-value jobs may dynamically add slices');\n\n let mostToTake = 1; // maximum number of slices we could take in per pile\n let payload = undefined; // used in return value\n let slicesTaken = 0; // number of slices in the pile already\n let pile = [];\n \n for (let slice of dataValues)\n {\n pile.push(slice);\n slicesTaken++;\n if (slicesTaken === mostToTake)\n {\n let total = await this.sliceUploadLogic(pile, mostToTake);\n payload = total.payload;\n \n if (total.newMostToTake < 0)\n {\n /* if total.newMostToTake == -1 (only non-positive value returned), then the pile was not successfully\n * uploaded because it was over the ceiling and we need to upload the pile *itself* again, recursively\n */\n payload = await this.addSlices(pile);\n /* and next time, the number of slices we take is the number from this time *divided* by the incrementFactor\n * since we know invariably that number of slices was under the ceiling AND target\n * if you're curious why that's an invariant, this is because mostToTake only ever *increases* by being multiplied by \n * a factor of incrementFactor within sliceUploadLogic, and this only occurs when the pile being uploaded that time\n * was under the target\n */\n mostToTake = mostToTake / dcpConfig.job.uploadIncreaseFactor;\n }\n else\n {\n /* in all other cases (other than the pile size being over the ceiling) the sliceUploadLogic helper \n * determines the number of slices we should pick up next time, so we just use the value it spits out\n */\n mostToTake = total.newMostToTake;\n }\n \n // reset slicesTaken and pile since at this point we know for sure the pile has been uploaded\n pile = [];\n slicesTaken = 0;\n }\n else\n {\n continue;\n }\n }\n // upload the pile one last time in case we continued off the last slice with a non-empty pile\n if (pile.length !== 0)\n {\n let finalObj = await this.sliceUploadLogic(pile, mostToTake);\n payload = finalObj.payload;\n mostToTake = finalObj.newMostToTake;\n \n if (mostToTake < 0)\n {\n // if you need documentation on the next two lines, look inside the if (total.newMostToTake < 0) just above\n payload = await this.addSlices(pile);\n mostToTake = mostToTake / dcpConfig.job.uploadIncreaseFactor;\n }\n }\n\n // and finally assign whatever mostToTake was at the end of this run of the function to be returned \n // as part of the payload in case addSlices was called recursively\n payload.mostToTake = mostToTake;\n \n /* contains the job's lastSliceNumber (the only externally-meaningful value returned from \n * the uploading of slices to the scheduler) in case the calling function needs it \n */\n return payload;\n }\n\n /**\n * job.snapshot(): Private function used to populate the payloadDetails from private data,\n * inferred data, etc. Once this function has run, the payloadDetails are\n * considered authoritatively up to date until the calling function returns\n * or awaits.\n */\n [SNAPSHOT]() {\n const pd = this[INTERNAL_SYMBOL].payloadDetails;\n\n pd.type = 'ad-hoc'; /* @todo implement appliances */\n pd.uuid = this.uuid;\n pd.workFunctionURI = this[INTERNAL_SYMBOL].workFunctionURI;\n pd.dependencies = this[INTERNAL_SYMBOL].dependencies;\n pd.requirePath = this[INTERNAL_SYMBOL].requirePath;\n pd.modulePath = this[INTERNAL_SYMBOL].modulePath;\n pd.resultStorageType = this[INTERNAL_SYMBOL].resultStorageType;\n pd.resultStorageDetails = this[INTERNAL_SYMBOL].resultStorageDetails;\n pd.resultStorageParams = this[INTERNAL_SYMBOL].resultStorageParams;\n pd.force100pctCPUDensity = this[INTERNAL_SYMBOL].force100pctCPUDensity;\n\n pd.requirements = this.requirements;\n applyObjectSchema(pd.requirements, DEFAULT_REQUIREMENTS, 'Requirements Object');\n \n // @todo: 'figure this out' - wise words from eddie /mp jan 2019\n if (!pd.options) { pd.options = {}; }\n if (!pd.public) { pd.public = {}; } \n\n for (let p of ['name', 'description', 'link']) {\n if (typeof this.public[p] === 'string') {\n pd.public[p] = this.public[p];\n }\n }\n\n // The max value that the client is willing to spend to deploy\n // (list on the scheduler, doesn't include compute payment)\n /// maxDeployPayment is the max the user is willing to pay to DCP (as a\n /// Hold), in addition to the per-slice offer and associated scrape.\n /// Currently calculated as `deployCost = costPerKB *\n /// (JSON.stringify(job).length / 1024) // 1e-9 per kb`\n // @todo: figure this out / er nov 2018\n pd.maxDeployPayment = 1;\n \n /// payloadDetails.timing can be provided as an initial estimate of slice time, to\n /// give a more useful useful calculated heap value (heap.value is more or less\n /// dcc-per-millisecond)\n pd.timing = pd.timing || 1; \n }\n\n /** Escrow additional funds for this job\n * @access public\n * @param {number|BigNumber} fundsRequired - A number or BigNumber instance representing the funds to escrow for this job\n */\n async escrow (fundsRequired) {\n if ((typeof fundsRequired !== 'number' && !BigNumber.isBigNumber(fundsRequired))\n || fundsRequired <= 0 || !Number.isFinite(fundsRequired) || Number.isNaN(fundsRequired)) {\n throw new Error(`Job.escrow: fundsRequired must be a number greater than zero. (not ${fundsRequired})`);\n }\n\n const response = await this.bankConnection.send('embiggenFeeStructure', {\n feeStructureAddress: this[INTERNAL_SYMBOL].payloadDetails.feeStructureId,\n additionalEscrow: BigNumber(fundsRequired),\n fromAddress: this.paymentAccountKeystore.address,\n }, this.paymentAccountKeystore);\n\n this.receipt = response.payload;\n\n return this.receipt;\n }\n\n async _pack () {\n var retval = __webpack_require__(/*! ./node-modules */ \"./src/dcp-client/job/node-modules.js\").createModuleBundle(this[INTERNAL_SYMBOL].dependencies);\n return retval;\n }\n\n /** \n * Collect all of the dependencies together, throw them into a BravoJS\n * module which sideloads them as a side effect of declaration, and transmit\n * them to the package manager. Then we return the package descriptor object,\n * which is guaranteed to have only one file in it.\n *\n * @returns {object} with properties name and files[0]\n */\n async _publishLocalModules() {\n const { tempFile, hash, unresolved } = await this._pack();\n\n if (!tempFile) {\n return { unresolved };\n }\n\n const sideloaderFilename = tempFile.filename;\n const pkg = {\n name: `dcp-pkg-v1-localhost-${hash.toString('hex')}`,\n version: '1.0.0',\n files: {\n [sideloaderFilename]: `${sideloaderModuleIdentifier}.js`,\n },\n }\n\n await dcpPublish.publish(pkg);\n tempFile.remove();\n\n return { pkg, unresolved };\n }\n\n /**\n * Deploys the job to the scheduler.\n * @param {number | object} [slicePaymentOffer=compute.marketValue] - Amount\n * in DCC that the user is willing to pay per slice.\n * @param {Keystore} [paymentAccountKeystore=wallet.get] - An instance of the\n * Wallet API Keystore that's used as the payment account when executing the\n * job.\n * @param {object} [initialSliceProfile] - An object describing the cost the\n * user believes the average slice will incur.\n * @access public\n * @emits Job#accepted\n */\n async exec(slicePaymentOffer = __webpack_require__(/*! ../compute */ \"./src/dcp-client/compute.js\").compute.marketValue, paymentAccountKeystore, initialSliceProfile) {\n if (this[INTERNAL_SYMBOL].connected) {\n throw new Error('Exec called twice on the same job handle.');\n }\n \n if (this.estimationSlices === Infinity)\n this[INTERNAL_SYMBOL].payloadDetails.estimationSlices = null;\n else if (this.estimationSlices <= 0)\n throw new Error('Incorrect value for estimationSlices; it can be an integer or Infinity!');\n else\n this[INTERNAL_SYMBOL].payloadDetails.estimationSlices = this.estimationSlices;\n\n /* eagerly connect to depedent services for better performance */\n this.eventSubscriber.eventRouterConnection.keepalive();\n this.deployConnection.keepalive();\n\n this.readyState = 'exec'\n this.emit('readystatechange', this.readyState)\n\n if (typeof slicePaymentOffer !== 'undefined') this.setSlicePaymentOffer(slicePaymentOffer);\n if (typeof initialSliceProfile !== 'undefined') this.initialSliceProfile = initialSliceProfile;\n if (typeof paymentAccountKeystore !== 'undefined') {\n /** XXX @todo deprecate use of ethereum wallet objects */\n if (typeof paymentAccountKeystore === 'object' && paymentAccountKeystore.hasOwnProperty('_privKey')) {\n console.warn('* deprecated API * - job.exec invoked with ethereum wallet object as paymentAccountKeystore') /* /wg oct 2019 */\n paymentAccountKeystore = paymentAccountKeystore._privKey\n }\n /** XXX @todo deprecate use of private keys */\n if (wallet.isPrivateKey(paymentAccountKeystore)) {\n console.warn('* deprecated API * - job.exec invoked with private key as paymentAccountKeystore') /* /wg dec 2019 */\n paymentAccountKeystore = await new wallet.Keystore(paymentAccountKeystore, '');\n }\n\n this.setPaymentAccountKeystore(paymentAccountKeystore)\n }\n\n // Unlock\n if (this[INTERNAL_SYMBOL].paymentAccountKeystore) {\n // Throws if they fail to unlock, we allow this since the keystore was set programmatically. \n await this[INTERNAL_SYMBOL].paymentAccountKeystore.unlock(undefined, parseFloat(dcpConfig.job.maxDeployTime));\n } else {\n // If not set programmatically, we keep trying to get an unlocked keystore ... forever.\n let locked = true;\n let safety = 0; // no while loop shall go unguarded\n let ks;\n do {\n ks = null;\n // custom message for the browser modal to denote the purpose of keystore submission\n let msg = `This application is requesting a keystore file to execute ${this.public.description || this.public.name || 'this job'}. Please upload the corresponding keystore file. If you upload a keystore file which has been encrypted with a passphrase, the application will not be able to use it until it prompts for a passphrase and you enter it.`;\n try {\n ks = await wallet.get({ contextId: this.contextId, jobName: this.public.name, msg});\n } catch (e) {\n if (e.code !== ClientModal.CancelErrorCode) throw e;\n };\n if (ks) {\n try {\n await ks.unlock(undefined, parseFloat(dcpConfig.job.maxDeployTime));\n locked = false;\n } catch (e) {\n const expectedCodes = [wallet.unlockFailErrorCode, ClientModal.CancelErrorCode];\n if (!expectedCodes.includes(e.code)) throw e;\n }\n }\n if (safety++ > 1000) throw new Error('EINFINITY: job.exec tried wallet.get more than 1000 times.')\n } while (locked);\n this.setPaymentAccountKeystore(ks)\n }\n\n // We either have a valid keystore + password or we have rejected by this point.\n if (!this.slicePaymentOffer) {\n throw new Error('A payment profile must be assigned before executing the job');\n } else {\n let pd = this[INTERNAL_SYMBOL].payloadDetails;\n pd.feeStructure = this[INTERNAL_SYMBOL].slicePaymentOffer.toFeeStructure(pd.data.length);\n }\n\n if (!this.address) {\n try {\n this.readyStateChange('init');\n await this[DEPLOY_JOB]();\n // localExec jobs are not entered in any compute group.\n if (!this[INTERNAL_SYMBOL].payloadDetails.localExec) {\n // remove the public compute group from the list of compute groups in case of infinite estimationSlices(DCP-2593)\n let computeGroupIds = this.computeGroups.map(prop => prop.id);\n if (this[INTERNAL_SYMBOL].payloadDetails.estimationSlices === null && computeGroupIds.includes(1))\n this.computeGroups = this.computeGroups.filter(item => item.id !== 1);\n // Add this job to its currently-defined compute groups (as well as public group, if included)\n await computeGroups.addJobToGroups(this.address, this.computeGroups);\n \n computeGroups\n .closeServiceConnection()\n .catch((err) =>\n console.error(\n 'Warning: could not close compute groups service connection',\n err,\n ),\n );\n }\n\n this.readyState = 'deployed';\n this.emit('readystatechange', this.readyState);\n this.emit('accepted');\n } catch (error) {\n if (ON_BROWSER) {\n await ClientModal.alert(error, { title: 'Failed to deploy job!' });\n }\n\n throw error;\n }\n } else {\n await this[ADD_LISTENERS]();\n\n this.readyState = 'reconnected';\n this.emit('readystatechange', this.readyState);\n }\n \n let data = this[INTERNAL_SYMBOL].dataValues;\n \n // if job data is by value then upload data to the scheduler in a staggered fashion\n if (Array.isArray(data))\n await this.addSlices(data).then(() => {\n return this.close();\n });\n\n this[ON_STATUS](this[INTERNAL_SYMBOL].payloadDetails.status);\n this[INTERNAL_SYMBOL].connected = true;\n\n return new Promise((resolve, reject) => {\n const onComplete = () => resolve(this.results);\n const onCancel = (event) => {\n /**\n * FIXME(DCP-1150): Remove this since normal cancel event is noisy\n * enough to not need stopped event too.\n */\n if (ON_BROWSER && !this[INTERNAL_SYMBOL].listeningForCancel)\n this[INTERNAL_SYMBOL].cancelAlert(event.reason);\n this.emit('cancel', event);\n\n let errorMsg = event.reason;\n if (event.error)\n errorMsg = errorMsg +`\\n Recent error massage: ${event.error.message}`\n \n reject(new DCPError(errorMsg, event.code));\n };\n\n this[INTERNAL_SYMBOL].events.once('stopped', async (stopEvent) => {\n if (this.receivedStop)\n {\n // The result submitter will ensure the client receives the stop event through the event router\n // by repeatedly sending stop messages if it detects something might have gone wrong. Sometimes\n // this detection is 'overeager', causing multiple stop events to be sent by the result submitter.\n // If multiple are received, ignore all after the first one.\n return;\n }\n this.receivedStop = true;\n this.emit('stopped', stopEvent.runStatus);\n switch (stopEvent.runStatus) {\n case jobStatus.finished:\n if (this.collateResults) {\n console.log(`1095: will get job info...`);\n let report = await this.getJobInfo();\n console.log(`1097: got job info`, report);\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 console.log(`1109: fetching ${remainSliceNumbers.length} more results from scheduler...`);\n const promises = remainSliceNumbers.map(sliceNumber => this.results.fetch([sliceNumber], true));\n await Promise.all(promises);\n console.log(`1112: fetched the results!`);\n }\n }\n \n this.emit('complete', this.results);\n onComplete();\n break;\n case jobStatus.cancelled:\n onCancel(stopEvent);\n break;\n default:\n /**\n * Asserting that we should never be able to reach here. The only\n * scheduler events that should trigger the Job's 'stopped' event\n * are jobStatus.cancelled, jobStatus.finished, and sliceStatus.paused.\n */\n reject(\n new Error(\n `Unknown event \"${stopEvent.runStatus}\" caused the job to be stopped.`,\n ),\n );\n break;\n }\n });\n\n if (!this[INTERNAL_SYMBOL].payloadDetails.running) {\n const runStatus = this[INTERNAL_SYMBOL].payloadDetails.runStatus;\n this[INTERNAL_SYMBOL].events.emit('stopped', { runStatus });\n }\n })\n .finally(() => {\n const handleErr = (e) => {\n console.error('Error while closing job connection:');\n console.error(e);\n }\n\n // Create an async IIFE to not block the promise chain\n (async () => {\n //delay to let last few events to be received\n await new Promise((resolve) => setTimeout(resolve, 1000));\n \n // close all of the connections so that we don't cause node processes to hang.\n await this.eventSubscriber.close().catch(handleErr);\n this.deployConnection.off('close', this.openDeployConn);\n await this.deployConnection.close().catch(handleErr);\n\n this.bankConnection.off('close', this.openDeployConn)\n await this.bankConnection.close().catch(handleErr);\n \n })();\n });\n }\n\n /**\n * job.addListeners(): Private function used to set up event listeners to the scheduler\n * before deploying the job.\n */\n async [ADD_LISTENERS] () {\n // This is important: We need to flush the task queue before adding listeners\n // because we queue pending listeners by listening to the newListener event (in the constructor).\n // If we don't flush here, then the newListener events may fire after this function has run,\n // and the events won't be properly set up.\n await new Promise(resolve => setTimeout(resolve, 0));\n\n // @todo: Listen for an estimated cost, probably emit an \"estimated\" event when it comes in?\n // also @todo: Do the estimation task(s) on the scheduler and send an \"estimated\" event\n\n // Always listen to the stop event. It will resolve the work function promise, so is always needed.\n this.on('stop', (ev) => {\n this[INTERNAL_SYMBOL].events.emit('stopped', ev)\n });\n\n // Connect listeners that were set up before exec\n const evts = Array.from(this[INTERNAL_SYMBOL].subscribedEvents);\n if (evts.includes('result'))\n this.listeningForResults = true;\n this[INTERNAL_SYMBOL].subscribedEvents.clear();\n await this[LISTEN_TO_EVENTS](evts);\n\n // Connect listeners that are set up after exec\n this.on('newListener', (evt) => {\n if (evt === 'newListener') return;\n this[LISTEN_TO_EVENTS]([evt]);\n });\n \n if (this.collateResults && !this.listeningForResults) {\n // automatically add a listener for results\n this.on('result', () => {});\n }\n\n // Connect work event listeners that were set up before exec\n const workEvts = Array.from(this[INTERNAL_SYMBOL].subscribedWorkerEvents);\n this[INTERNAL_SYMBOL].subscribedWorkerEvents.clear();\n for (let evt of workEvts) {\n await this[LISTEN_TO_WORK_EVENTS](evt);\n }\n\n // Connect work event listeners that are set up after exec\n this.work.on('newListener', (evt) => {\n if (evt === 'newListener') return;\n this[LISTEN_TO_WORK_EVENTS](evt);\n });\n }\n\n /**\n * Subscribes to either reliable events or optional events\n * @param {string[]} events \n */\n async [LISTEN_TO_EVENTS](events) {\n\n const reliableEvents = [];\n const optionalEvents = [];\n for (let eventName of events) {\n eventName = eventName.toLowerCase();\n if (this[INTERNAL_SYMBOL].subscribedEvents.has(eventName))\n {\n // already subscribed to this event\n continue;\n }\n else\n {\n this[INTERNAL_SYMBOL].subscribedEvents.add(eventName);\n \n if (this.eventTypes[eventName] && this.eventTypes[eventName].reliable)\n {\n reliableEvents.push(eventName)\n }\n else if (this.eventTypes[eventName] && !this.eventTypes[eventName].reliable)\n {\n optionalEvents.push(eventName)\n }\n else\n {\n // console.debug('606: listening for unexpected/unsupported event:', eventName);\n }\n }\n }\n await this.eventSubscriber.subscribeManyEvents(reliableEvents, optionalEvents, { filter: { job: this.address } })\n }\n\n /**\n * Establishes listeners for worker events when requested by the client\n * @param {string} eventName \n */\n async [LISTEN_TO_WORK_EVENTS](eventName) {\n if (this[INTERNAL_SYMBOL].subscribedWorkerEvents.has(eventName)) {\n // already subscribed to this event\n return;\n }\n else\n {\n this[INTERNAL_SYMBOL].subscribedWorkerEvents.add(eventName);\n this.eventIntercepts.custom = (ev) => this.work.emit(eventName, ev)\n const optionalEvents = ['custom'];\n await this.eventSubscriber.subscribeManyEvents([], optionalEvents, { filter: { job: this.address } });\n }\n }\n\n /**\n * Takes result events as input, stores the result and fires off\n * events on the job handle as required. (result, duplicate-result)\n *\n * @param {object} ev - the event recieved from protocol.listen('/results/0xThisGenAdr')\n */\n async [ON_RESULT] (ev) {\n if (this[INTERNAL_SYMBOL].results === null) {\n // This should never happen - the onResult event should only be established/called\n // in addListeners which should also initialize the internal results array\n throw new Error('Job.onResult was invoked before initializing internal results');\n }\n \n const { result: _result, time } = ev.result;\n let result = await fetchURI(_result);\n\n if (this[INTERNAL_SYMBOL].results[ev.sliceNumber]) {\n const changed = JSON.stringify(this[INTERNAL_SYMBOL].results[ev.sliceNumber]) !== JSON.stringify(result);\n this.emit('duplicate-result', { sliceNumber: ev.sliceNumber, changed });\n }\n\n this[INTERNAL_SYMBOL].results[ev.sliceNumber] = result;\n this[INTERNAL_SYMBOL].resultsAvailable[ev.sliceNumber] = true;\n this.emit('result', { sliceNumber: ev.sliceNumber, result });\n this.emit('resultsUpdated');\n }\n\n /**\n * Receives status events from the scheduler, updates the local status object\n * and emits a 'status' event\n *\n * @param {object} ev - the status event received from\n * protocol.listen('/status/0xThisGenAdr')\n * @param {boolean} emitStatus - value indicating whether or not the status\n * event should be emitted\n */\n [ON_STATUS]({ runStatus, total, distributed, computed }, emitStatus = true) {\n Object.assign(this[INTERNAL_SYMBOL].status, {\n runStatus,\n total,\n distributed,\n computed,\n });\n\n if (emitStatus) {\n this.emit('status', this.status);\n }\n }\n\n /**\n * Sends a request to the scheduler to deploy the job.\n */\n async [DEPLOY_JOB] () {\n const { payloadDetails } = this[INTERNAL_SYMBOL];\n \n this[SNAPSHOT](); /* .payloadDetails now up to date */\n \n /* Send sideloader bundle to the package server */\n if (DCP_ENV.platform === 'nodejs' && this[INTERNAL_SYMBOL].dependencies.length) {\n let {pkg, unresolved} = await this._publishLocalModules();\n\n payloadDetails.dependencies = unresolved;\n if (pkg)\n payloadDetails.dependencies.push(pkg.name + '/' + sideloaderModuleIdentifier);\n }\n \n this.readyState = 'preauth';\n this.emit('readystatechange', this.readyState);\n\n /* eagerly connect to dependent services for better performance */\n computeGroups.keepAlive()\n\n const adhocId = payloadDetails.uuid.slice(payloadDetails.uuid.length - 6, payloadDetails.uuid.length);\n const schedId = await dcpConfig.scheduler.identity;\n const myId = await wallet.getId();\n const preauthToken = await bankUtil.preAuthorizePayment(schedId, payloadDetails.maxDeployPayment, this.paymentAccountKeystore);\n const { dataRange, dataValues, dataPattern, sliceCount } = marshalInputData(payloadDetails.data);\n if(dataValues)\n this[INTERNAL_SYMBOL].dataValues = dataValues;\n \n this.readyState = 'deploying';\n this.emit('readystatechange', this.readyState);\n \n /* Payload format is documented in scheduler-v4/libexec/job-submit/operations/submit.js */\n const submitPayload = {\n owner: myId.address,\n paymentAccount: this.paymentAccountKeystore.address,\n priority: 0, // @nyi\n\n workFunctionURI: payloadDetails.workFunctionURI,\n uuid: payloadDetails.uuid,\n mvMultSlicePayment: +payloadDetails.feeStructure.marketValue || 0, // @todo: improve feeStructure internals to better reflect v4\n absoluteSlicePayment: +payloadDetails.feeStructure.maxPerRequest || 0,\n requirePath: payloadDetails.requirePath,\n modulePath: payloadDetails.modulePath,\n dependencies: payloadDetails.dependencies,\n requirements: payloadDetails.requirements, /* capex */\n localExec: payloadDetails.localExec,\n force100pctCPUDensity: this.force100pctCPUDensity,\n estimationSlices: payloadDetails.estimationSlices,\n workerConsole: this.workerConsole,\n\n description: payloadDetails.public.description || 'Discreetly making the world smarter',\n name: payloadDetails.public.name || 'Ad-Hoc Job' + adhocId,\n \n preauthToken, // XXXwg/er @todo: validate this after fleshing out the stub(s)\n\n resultStorageType: payloadDetails.resultStorageType, // @todo: implement other result types\n resultStorageDetails: payloadDetails.resultStorageDetails, // Content depends on resultStorageType\n resultStorageParams: payloadDetails.resultStorageParams, // Post params for off-prem storage\n dataRange,\n dataPattern,\n sliceCount\n };\n\n /* Determine thee type of the arguments option and set the submit message payload accordingly. */\n if (Array.isArray(payloadDetails.arguments) && payloadDetails.arguments.length === 1 && payloadDetails.arguments[0] instanceof DcpURL) {\n submitPayload.arguments = payloadDetails.arguments[0].href;\n } else if (payloadDetails.arguments instanceof RemoteDataSet) {\n submitPayload.marshaledArguments = kvin.marshal(payloadDetails.arguments.map(e => new URL(e)))\n } else if (payloadDetails.arguments) {\n try {\n submitPayload.marshaledArguments = kvin.marshal(Array.from(payloadDetails.arguments));\n } catch(e) {\n throw new Error(`Could not convert job arguments to Array (${e.message})`);\n }\n }\n\n // XXXpfr Excellent tracing.\n if (debugging('dcp-client')) {\n dumpObject(submitPayload, 'Submit: Job Index: Examine submitPayload', 128);\n }\n \n // Deploy the job!\n const deployed = await this.deployConnection.send('submit', submitPayload, myId);\n\n if (!deployed.success) {\n if(deployed.payload.code === 'ENOTFOUND') {\n throw new DCPError(`Failed to submit job to scheduler. Account: ${submitPayload.paymentAccount} was not found or does not have sufficient balance (${deployed.payload.info.deployCost} DCCs needed to deploy this job)`, deployed.payload);\n } else {\n throw new DCPError('Failed to submit job to scheduler', deployed.payload);\n }\n }\n\n this.address = payloadDetails.address = deployed.payload.job;\n this[INTERNAL_SYMBOL].deployCost = deployed.payload.deployCost;\n \n if (!payloadDetails.status)\n payloadDetails.status = {\n runStatus: null,\n total: 0,\n computed: 0,\n distributed: 0,\n };\n \n payloadDetails.runStatus = payloadDetails.status.runStatus = deployed.payload.status;\n payloadDetails.status.total = deployed.payload.lastSliceNumber;\n payloadDetails.running = true;\n \n this.readyState = 'listeners';\n\n this.emit('readystatechange', this.readyState);\n\n const listenersP = this[ADD_LISTENERS]();\n\n this[INTERNAL_SYMBOL].payloadDetails = {\n ...this[INTERNAL_SYMBOL].payloadDetails,\n ...payloadDetails,\n };\n \n return listenersP;\n }\n\n /**\n * This function is identical to exec, except that the job is executed locally\n * in the client.\n * @async\n * @param {number} cores - the number of local cores in which to execute the job.\n * @param {...any} args - The remaining arguments are identical to the arguments of exec\n * @return {Promise<ResultHandle>} - resolves with the results of the job, rejects on an error\n * @access public\n */\n localExec (cores = 1, ...args) {\n this[INTERNAL_SYMBOL].payloadDetails.localExec = true;\n this[INTERNAL_SYMBOL].payloadDetails.estimationSlices = 0;\n\n let worker;\n this.on('accepted', () => {\n // Start a worker for this job\n worker = new Worker({\n localExec: true,\n jobAddresses: [this.address],\n paymentAddress: this[INTERNAL_SYMBOL].paymentAccountKeystore.address,\n maxWorkingSandboxes: cores,\n sandboxOptions: {\n ignoreNoProgress: true,\n SandboxConstructor: (DCP_ENV.platform === 'nodejs' &&\n __webpack_require__(/*! ../worker/evaluators */ \"./src/dcp-client/worker/evaluators/index.js\").nodeEvaluatorFactory())\n },\n });\n\n worker.start().catch((e) => {\n console.error(\"Failed to start worker for localExec:\");\n console.error(e.message);\n });\n });\n\n return this.exec(...args).finally(() => {\n if (worker) {\n setTimeout(() => {\n // stop the worker\n worker.stop(true);\n }, 3000);\n }\n });\n }\n\n /**\n * The current job status. Will have undefined values when the handle hasn't had exec called on it yet.\n * @access public\n * @readonly\n * @type {object}\n * @property {number} total Total number of slices in the job\n * @property {number} distributed Number of slices that have been distributed\n * @property {number} computed Number of slices that have completed execution (returned a result)\n * @property {string} runStatus Current runStatus of the job\n */\n get status () {\n // Shallow-copy to prevent modification\n return { ...this[INTERNAL_SYMBOL].status };\n }\n\n get requirePath() {\n return this[INTERNAL_SYMBOL].requirePath;\n }\n\n /**\n * This function specifies a module dependency (when the argument is a string)\n * or a list of dependencies (when the argument is an array) of the work\n * function. This function can be invoked multiple times before deployment.\n * @param {string | string[]} modulePaths - A string or array describing one\n * or more dependencies of the job.\n * @access public\n */\n requires(modulePaths) {\n if (\n typeof modulePaths !== 'string' &&\n (!Array.isArray(modulePaths) ||\n modulePaths.some((modulePath) => typeof modulePath !== 'string'))\n ) {\n throw new TypeError(\n 'The argument to dependencies is not a string or an array of strings',\n );\n } else if (modulePaths.length === 0) {\n throw new RangeError(\n 'The argument to dependencies cannot be an empty string or array',\n );\n } else if (\n Array.isArray(modulePaths) &&\n modulePaths.some((modulePath) => modulePath.length === 0)\n ) {\n throw new RangeError(\n 'The argument to dependencies cannot be an array containing an empty string',\n );\n }\n\n if (!Array.isArray(modulePaths)) {\n modulePaths = [modulePaths];\n }\n\n for (const modulePath of modulePaths) {\n if (modulePath[0] !== '.' && modulePath.indexOf('/') !== -1) {\n const modulePrefixRegEx = /^(.*)\\/.*?$/;\n const [, modulePrefix] = modulePath.match(modulePrefixRegEx);\n if (\n modulePrefix &&\n this[INTERNAL_SYMBOL].requirePath.indexOf(modulePrefix) === -1\n ) {\n this[INTERNAL_SYMBOL].requirePath.push(modulePrefix);\n }\n }\n\n this[INTERNAL_SYMBOL].dependencies.push(modulePath);\n }\n }\n\n get slicePaymentOffer () {\n return this[INTERNAL_SYMBOL].slicePaymentOffer;\n }\n\n /**\n * The keystore that will be used to pay for the job. Can be set with {@link Job.setPaymentAccountKeystore} or by providing a keystore to {@link Job.exec}.\n * @readonly\n * @access public\n * @type {module:dcp/wallet.AuthKeystore}\n */\n get paymentAccountKeystore () {\n return this[INTERNAL_SYMBOL].paymentAccountKeystore;\n }\n\n /** Set the account upon which funds will be drawn to pay for the job.\n * @param {module:dcp/wallet.AuthKeystore} keystore A keystore that representa a bank account.\n * @access public\n */\n setPaymentAccountKeystore (keystore) {\n if (this.address) {\n if (!keystore.address.eq(this[INTERNAL_SYMBOL].payloadDetails.owner)) {\n let message = \"Cannot change payment account after job has been deployed\";\n this.emit('EPERM', message);\n throw new Error(`EPERM: ${message}`);\n }\n }\n \n if (!(keystore instanceof wallet.Keystore)) {\n let e = new Error('Not an instance of Keystore: ' + keystore.toString())\n console.log(`Not an instance of Keystore: ${keystore}`)\n throw e\n }\n this[INTERNAL_SYMBOL].paymentAccountKeystore = keystore;\n }\n\n /** Set the slice payment offer. This is equivalent to the first argument to exec.\n * @param {number} slicePaymentOffer - The number of DCC the user is willing to pay to compute one slice of this job\n */\n setSlicePaymentOffer (slicePaymentOffer) {\n this[INTERNAL_SYMBOL].slicePaymentOffer = new SlicePaymentOffer(slicePaymentOffer);\n }\n\n /**\n * @param {URL} locationUrl - A URL object \n * @param {object} postParams - An object with any parameters that a user would like to be passed to a \n * remote result location. This object is capable of carry API keys for S3, \n * DropBox, etc. These parameters are passed as parameters in an \n * application/x-www-form-urlencoded request.\n */\n setResultStorage(locationUrl, postParams) {\n if (locationUrl instanceof URL || locationUrl instanceof DcpURL) {\n this[INTERNAL_SYMBOL].resultStorageDetails = locationUrl;\n } else {\n const e = new Error('Not an instance of a DCP URL: ' + locationUrl);\n console.log('Not an instance of a DCP URL ' + locationUrl);\n throw e;\n }\n\n // resultStorageParams contains any post params required for off-prem storage\n if (typeof postParams !== 'undefined' && typeof postParams === 'object' ) {\n this[INTERNAL_SYMBOL].resultStorageParams = postParams;\n } else {\n const e = new Error('Not an instance of a object: ' + postParams);\n console.log('Not an instance of an object ' + postParams);\n throw e;\n } \n\n // Some type of object here\n this[INTERNAL_SYMBOL].resultStorageType = 'pattern';\n }\n\n /** close an open job to indicate we are done adding data so it is okay to finish\n * the job at the appropriate time\n */\n async close() {\n return this.deployConnection.send('closeJob', {\n job: this.id,\n });\n }\n}\n\n/**\n * @typedef {object} MarshaledInputData\n * @property {any[]} [dataValues]\n * @property {string} [dataPattern]\n * @property {RangeObject} [dataRange]\n * @property {number} [sliceCount]\n */\n\n/**\n * Depending on the shape of the job's data, resolve it into a RangeObject, a\n * Pattern, or a values array, and return it in the appropriate property.\n *\n * @param {any} data Job's input data\n * @return {MarshaledInputData} An object with one of the following properties set:\n * - dataValues: job input is an array of arbitrary values \n * - dataPattern: job input is a URI pattern \n * - dataRange: job input is a RangeObject (and/or friends)\n */\nfunction marshalInputData(data) {\n if (!(data instanceof Object\n || data instanceof SuperRangeObject)) {\n throw new TypeError(`Invalid job data type: ${typeof data}`);\n }\n\n /**\n * @type MarshaledInputData\n */\n const marshalledInputData = {};\n\n // TODO(wesgarland): Make this more robust.\n if (data instanceof SuperRangeObject ||\n (data.hasOwnProperty('ranges') && data.ranges instanceof MultiRangeObject) ||\n (data.hasOwnProperty('start') && data.hasOwnProperty('end'))) {\n marshalledInputData.dataRange = data;\n } else if (Array.isArray(data)) {\n marshalledInputData.dataValues = data;\n } else if (data instanceof URL || data instanceof DcpURL) {\n marshalledInputData.dataPattern = String(data);\n } else if(data instanceof RemoteDataSet) {\n marshalledInputData.dataValues = data.map(e => new URL(e));\n } else if(data instanceof RemoteDataPattern) {\n marshalledInputData.dataPattern = data['pattern'];\n marshalledInputData.sliceCount = data['sliceCount'];\n }\n\n log('marshalledInputData:', marshalledInputData);\n return marshalledInputData;\n}\n\nObject.assign(exports, {\n Job,\n SlicePaymentOffer,\n ResultHandle,\n});\n\n\n//# sourceURL=webpack:///./src/dcp-client/job/index.js?");
|
|
5086
5086
|
|
|
5087
5087
|
/***/ }),
|
|
5088
5088
|
|
|
@@ -5182,7 +5182,7 @@ eval("/* WEBPACK VAR INJECTION */(function(process) {/**\n * @file /src/sch
|
|
|
5182
5182
|
/*! no static exports found */
|
|
5183
5183
|
/***/ (function(module, exports, __webpack_require__) {
|
|
5184
5184
|
|
|
5185
|
-
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=
|
|
5185
|
+
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=afa05a15457fc3feb611804dc4f2159bba2ff452,' + Date.now() + hash;\n\n window.location.replace(newUrl);\n }\n}\n\nObject.assign(module.exports, {\n SchedMsgWeb\n});\n\n\n//# sourceURL=webpack:///./src/dcp-client/schedmsg/schedmsg-web.js?");
|
|
5186
5186
|
|
|
5187
5187
|
/***/ }),
|
|
5188
5188
|
|
|
@@ -5351,7 +5351,7 @@ eval("/**\n * @file This module implements the Worker API, used to create worker
|
|
|
5351
5351
|
/***/ (function(module, exports, __webpack_require__) {
|
|
5352
5352
|
|
|
5353
5353
|
"use strict";
|
|
5354
|
-
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\n /** @type {Slice} */\n this.slice = null;\n\n /** @type {number?} */\n this.started = null;\n /** @type {number?} */\n this.sliceStartTime = null;\n /** @type {boolean} */\n this.requiresGPU = false;\n /** @type {string|URL} */\n this.packageURL = dcpConfig.packageManager.location\n /** @type {number?} */\n this.id = Sandbox.getNewId();\n\n this.ringMessageHandlers = [\n this.handleRing0Message,\n this.handleRing1Message,\n this.handleRing2Message,\n this.handleRing3Message,\n ];\n\n this.resetSliceTimeReport();\n }\n\n static getNewId() {\n return Sandbox.idCounter++;\n }\n\n get state () {\n return this._state\n }\n\n set state (value) {\n if (Sandbox.debugState) {\n console.debug(`sandbox - changing state of ${this.id}... ${this._state} -> ${value}`)\n }\n\n if (this.state === TERMINATED && value !== TERMINATED) {\n // For safety!\n throw new Error(`Sandbox set state violation, attepted to change state from ${this.state} to ${value}`);\n }\n\n this._state = value;\n }\n\n get isReadyForAssign () {\n return this.state === READY_FOR_ASSIGN;\n }\n\n get isAssigned () {\n return this.state === ASSIGNED;\n }\n\n get isWorking () {\n return this.state === WORKING;\n }\n\n get isTerminated () {\n return this.state === TERMINATED;\n }\n\n changeWorkingToAssigned () {\n if (this.isWorking)\n this.state = ASSIGNED;\n }\n\n setIsAssigning () {\n this.state = ASSIGNING;\n }\n\n /**\n * Readies the sandbox. This will result in the sandbox being ready and not assigned,\n * it will need to be assigned with a job before it is able to do work.\n *\n * @todo maybe preload specific modules or let the cache pass in what modules to load?\n * @throws on failure to ready\n */\n async start(delay = 0) {\n this.started = Date.now();\n this.state = READYING;\n\n if (delay > 0) await new Promise((resolve) => setTimeout(resolve, delay * timeDilation));\n\n try {\n // RING 0\n this.evaluatorHandle = new this.options.SandboxConstructor({\n name: `DCP Sandbox #${this.id}`,\n });\n this.evaluatorHandle.onerror = this.onerror.bind(this);\n\n const messageHandler = this.onmessage.bind(this);\n this.evaluatorHandle.onmessage = function onmessage(event)\n {\n let data;\n if (event.data.serialized)\n {\n data = kvin.parse(event.data.message);\n }\n else\n {\n data = kvin.unmarshal(event.data);\n }\n messageHandler({ data });\n }\n\n const evaluatorPostMessage = this.evaluatorHandle.postMessage.bind(this.evaluatorHandle);\n this.evaluatorHandle.postMessage = function postMessage(message)\n {\n evaluatorPostMessage(scopedKvin.marshal(message));\n }\n\n const ceci = this;\n this.evaluatorHandle.addEventListener('end', () => ceci.terminate(true));\n\n // Now in RING 1\n\n // Now in RING 2\n await this.describe();\n this.state = READY_FOR_ASSIGN;\n this.emit('ready', this);\n } catch (error) {\n console.warn('Failed to start the sandbox -', error.message);\n this.terminate(false);\n throw error;\n }\n }\n\n /**\n * This will assign the sandbox with a job, loading its sandbox code\n * into the sandbox.\n *\n * @param {string} jobAddress The address of the job to assign to\n * @throws on initialization failure\n */\n async assign(jobAddress) {\n this.jobAddress = jobAddress;\n this.job = await this.supervisorCache.fetchJob(jobAddress, this.allowedOrigins);\n\n assertEq3(this.job.address, jobAddress);\n assert(typeof this.job === 'object');\n assert(typeof this.job.requirements === 'object');\n assert(Array.isArray(this.job.dependencies));\n assert(Array.isArray(this.job.requirePath));\n\n // Extract public data from job, with defaults\n this.public = Object.assign({\n name: `Anonymous Job ${this.job.address.slice(0, 6)}`,\n description: 'Discreetly helping make the world smarter.',\n link: 'https://distributed.computer/about',\n }, this.job.public);\n\n // Future: We may want other filename tags for appliances // RR Nov 2019\n\n // Important: The order of applying requirements before loading the sandbox code\n // is important for modules and sandbox code to set globals over the whitelist.\n await this.applySandboxRequirements(this.job.requirements);\n await this.assignEvaluator();\n this.state = ASSIGNED;\n }\n\n async assignEvaluator() {\n debug('Begin assigning job to evaluator');\n const ceci = this;\n\n return new Promise(function sandbox$$assignEvaluatorPromise(resolve, reject) {\n const message = {\n request: 'assign',\n job: ceci.job,\n sandboxConfig: {\n worker: dcpConfig.worker,\n },\n };\n\n const onSuccess = (event) => {\n // eslint-disable-next-line no-use-before-define\n ceci.ee.removeListener('reject', onFailListener);\n ceci.emit('assigned', event.jobAddress);\n debug('Job assigned to evaluator');\n resolve();\n };\n\n const onFail = (error) => {\n // eslint-disable-next-line no-use-before-define\n ceci.ee.removeListener('assigned', onSuccessListener);\n reject(error);\n };\n\n const onSuccessListener = ceci.ee.once('assigned', onSuccess);\n const onFailListener = ceci.ee.once('reject', onFail);\n ceci.evaluatorHandle.postMessage(message);\n });\n }\n\n /**\n * Evaluates a string inside the sandbox.\n *\n * @param {string} code - the code to evaluate in the sandbox\n * @param {string} filename - the name of the 'file' to help with debugging,\n * no longer working though?\n * @returns {Promise} - resolves with eval result on success, rejects\n * otherwise\n */\n eval(code, filename) {\n var ceci = this;\n \n return new Promise(function sandbox$$eval$Promise(resolve, reject) {\n let msgId = nanoid();\n let msg = {\n request: 'eval',\n data: code,\n filename,\n msgId, \n }\n\n const eventId = EVAL_RESULT_PREFIX + msgId;\n\n let onSuccess = (event) => {\n ceci.ee.removeListener('reject', onFailListener)\n resolve(event)\n }\n\n let onFail = (event) => {\n ceci.ee.removeListener(eventId, onSuccessListener)\n reject(event)\n }\n\n let onSuccessListener = ceci.ee.once(eventId, onSuccess);\n let onFailListener = ceci.ee.once('reject', onFail)\n\n ceci.evaluatorHandle.postMessage(msg)\n })\n }\n\n /**\n * Resets the state of the bootstrap, without resetting the sandbox function if assigned.\n * Mostly used to reset the progress status before reusing a sandbox on another slice.\n * Must be called after @start.\n *\n * @returns {Promise} - resolves with result on success, rejects otherwise\n */\n resetSandboxState () {\n var ceci = this;\n\n return new Promise(function sandbox$resetSandboxStatePromise(resolve, reject) {\n let successCb, failTimeout;\n let msg = {\n request: 'resetState',\n };\n\n successCb = ceci.ee.once('resetStateDone', function sandbox$resetSandboxState$success () {\n if (failTimeout === false)\n return; /* already rejected */\n clearTimeout(failTimeout);\n failTimeout = false;\n resolve();\n });\n\n failTimeout = setTimeout(function sandbox$resetSandboxState$fail() {\n if (failTimeout === false)\n return; /* already resolved */\n \n ceci.ee.removeListener('resetStateDone', successCb);\n ceci.terminate(false);\n failTimeout = false;\n\n reject(new Error('resetState never received resetStateDone event from sandbox'));\n }, 3000 * timeDilation); /* XXXwg need tuneable */\n\n assert(ceci.evaluatorHandle); // It is possible that ceci.terminate nulls out evaluatorHandle before getting here.\n ceci.evaluatorHandle.postMessage(msg);\n });\n }\n\n /**\n * Clear all timers that are set inside the sandbox (evaluator) environment.\n *\n * @returns {Promise} - resolves with result on success, rejects otherwise\n */\n clearSandboxTimers() {\n var ceci = this;\n \n return new Promise(function sandbox$clearSandboxTimersPromise(resolve, reject) {\n let successCb, failTimeout;\n let msg = {\n request: 'clearTimers',\n };\n\n successCb = ceci.ee.once('clearTimersDone', function sandbox$clearSandboxTimers$success() {\n if (failTimeout === false)\n return; /* already rejected */\n clearTimeout(failTimeout);\n failTimeout = false;\n resolve();\n });\n\n failTimeout = setTimeout(function sanbox$clearSandboxTimers$fail() {\n if (failTimeout === false)\n return; /* already resolved */\n \n ceci.ee.removeListener('clearTimersDone', successCb);\n ceci.terminate(false);\n failTimeout = false;\n \n reject(new Error('clearTimers never received clearTimersDone event from sandbox'));\n }, 3000 * timeDilation); /* XXXwg need tuneable */\n\n if (ceci.evaluatorHandle) // Sometimes ceci.terminate nulls out evaluatorHandle before getting here.\n ceci.evaluatorHandle.postMessage(msg);\n });\n }\n\n /**\n * Sends a post message to describe its capabilities.\n *\n * Side effect: Sets the capabilities property of the current sandbox.\n *\n * @returns {Promise} Resolves with the sandbox's capabilities. Rejects with\n * an error saying a response was not received.\n * @memberof Sandbox\n */\n describe() {\n debug('Beginning to describe evaluator');\n var ceci = this;\n \n return new Promise(function sandbox$describePromise(resolve, reject) {\n if (ceci.evaluatorHandle === null) {\n return reject(new Error('Evaluator has not been initialized.'));\n }\n\n /**\n * Opted to create a flag for the describe response being received so that\n * we don't have to *hoist* the timeout's id to clear it in the response\n * handler.\n */\n let didReceiveDescribeResponse = false;\n const describeResponseHandler = ceci.ee.once('describe', (data) => {\n didReceiveDescribeResponse = true;\n const { capabilities } = data;\n if (typeof capabilities === 'undefined') {\n reject(\n new Error('Did not receive capabilities from describe response.'),\n );\n }\n ceci.capabilities = capabilities;\n\n // Currently only used in tests. May use the event in the future.\n ceci.emit('described', capabilities);\n debug('Evaluator has been described');\n resolve(capabilities);\n });\n const describeResponseFailedHandler = () => {\n if (!didReceiveDescribeResponse) {\n ceci.ee.removeListener('describe', describeResponseHandler);\n ceci.terminate(false);\n reject(\n new Error(\n 'Describe message timed-out. No describe response was received from the describe command.',\n ),\n );\n }\n };\n\n const message = {\n request: 'describe',\n };\n\n // Arbitrarily set the waiting time.\n setTimeout(describeResponseFailedHandler, 6000 * timeDilation); /* XXXwg need tuneable */\n assert(ceci.evaluatorHandle); // It is possible that ceci.terminate nulls out evaluatorHandle before getting here.\n ceci.evaluatorHandle.postMessage(message);\n });\n }\n\n /**\n * Passes the job's requirements object into the sandbox so that the global\n * access lists can be updated accordingly.\n *\n * e.g. disallow access to OffscreenCanvas without\n * environment.offscreenCanvas=true present.\n *\n * Must be called after @start.\n *\n * @returns {Promise} - resolves with result on success, rejects otherwise\n */\n applySandboxRequirements(requirements) {\n var ceci = this;\n \n return new Promise(function sandbox$applySandboxRequirementsPromise(resolve, reject) {\n const message = {\n requirements,\n request: 'applyRequirements',\n };\n let wereRequirementsApplied = false;\n\n const successCb = ceci.ee.once(\n 'applyRequirementsDone',\n function sandbox$applyRequirements$success() {\n wereRequirementsApplied = true;\n resolve();\n },\n );\n\n assert(typeof message.requirements === 'object');\n ceci.evaluatorHandle.postMessage(message);\n\n setTimeout(function sandbox$finishApplySandboxRequirements() {\n if (!wereRequirementsApplied) {\n ceci.ee.removeListener('applyRequirementsDone', successCb);\n ceci.terminate(false);\n reject(\n new Error(\n 'applyRequirements never received applyRequirementsDone response from sandbox',\n ),\n );\n }\n }, 3000 * timeDilation); /* XXXwg needs tunable */\n });\n }\n\n /**\n * Executes a slice received from the supervisor.\n * Must be called after @start.\n *\n * @param {Slice} slice - bare minimum data required for the job/job code to be executed on\n * @param {number} [delay = 0] the delay that this method should wait before beginning work, used to avoid starting all sandboxes at once\n *\n * @returns {Promise} - resolves with result on success, rejects otherwise\n */\n\n async work (slice, delay = 0) {\n var ceci = this;\n\n if (!ceci.isAssigned) {\n throw new Error(\"Sandbox.run: Sandbox is not ready to work, state=\" + ceci.state);\n }\n\n ceci.state = WORKING;\n ceci.slice = slice;\n assert(slice);\n\n // cf. DCP-1720\n this.resetSliceTimeReport();\n \n // Now wait for the delay if provided, prevents many sandboxes starting at once from crashing the supervisor\n if (delay > 0) await new Promise(resolve => setTimeout(resolve, (delay + 1) * timeDilation));\n if (!ceci.isWorking) return; // sandbox.terminate could have been called during the delay timeout\n\n // Prepare the sandbox to begin work\n // will be replaced by `assign` message that should be called before emitting a `work` message\n if (ceci.jobAddress !== slice.jobAddress) {\n throw new Error(`Sandbox.run: Sandbox is already assigned and jobAddress doesn't match previous (${ceci.jobAddress} !== ${slice.jobAddress})`);\n }\n\n let sliceHnd = { job: ceci.public, sandbox: ceci };\n await ceci.resetSandboxState();\n if (!ceci.slice) {\n console.error(`Slice for job ${ceci.jobAddress} vanished during work initialization - aborting`);\n return;\n }\n\n let inputDatum;\n let dataError = false;\n try {\n 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 = ceci.supervisorCache.cache.job[ceci.jobAddress].mro;\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 // Ceci is the reject callback for when the slice throws an error\n ceci.terminate(false);\n\n ceci.emit('error', err, 'slice');\n\n if (err instanceof NoProgressError) {\n ceci.emit('workEmit', {\n eventName: 'noProgress',\n payload: {\n timestamp: Date.now() - ceci.sliceStartTime,\n data: ceci.slice.datumUri,\n progressReports: ceci.progressReports,\n }\n });\n }\n throw err;\n })\n .finally(function sandbox$$work$finally() {\n ceci.emit('end', sliceHnd);\n });\n }\n\n resetProgressTimeout() {\n if (this.progressTimeout) {\n clearTimeout(this.progressTimeout);\n this.progressTimeout = null;\n }\n\n this.progressTimeout = setTimeout(() => {\n if (this.options.ignoreNoProgress) {\n return console.warn(\"ENOPROGRESS silenced by localExec: In a remote worker, this slice would be stopped for not calling progress frequently enough.\");\n }\n\n this.ee.emit('reject', new NoProgressError(`No progress event was received in the last ${dcpConfig.worker.sandbox.progressTimeout / 1000} seconds.`));\n }, +dcpConfig.worker.sandbox.progressTimeout * timeDilation);\n }\n\n resetSliceTimeout() {\n if (this.sliceTimeout) clearTimeout(this.sliceTimeout);\n\n this.sliceTimeout = setTimeout(() => {\n if (Sandbox.debugWork) return console.warn(\"Sandbox.debugWork: Ignoring slice timeout\");\n\n this.ee.emit('reject', new SliceTooSlowError(`Slice took longer than ${dcpConfig.worker.sandbox.sliceTimeout / 1000} seconds.`));\n }, +dcpConfig.worker.sandbox.sliceTimeout * timeDilation);\n }\n \n async handleRing0Message(data) {\n debugging('event:ring-0') && debug('event:ring-0', data);\n //handling a true ring 0 message\n switch (data.request) {\n case 'sandboxLoaded':\n // emit externally\n this.emit('sandboxLoaded', this)\n break;\n\n case 'scriptLoaded':\n // emit externally\n this.emit('scriptLoaded', data);\n \n if(data.result !== \"success\") {\n this.onerror(data);\n }\n break;\n \n case 'clearTimersDone':\n this.ee.emit(data.request, data);\n break;\n case 'totalCPUTime':\n this.updateTime(data);\n this.completeData.timeReport = this.sliceTimeReport\n this.ee.emit('resolve', this.completeData);\n delete this.completeData;\n break;\n case 'error':\n // Warning: rejecting here with just event.data.error causes issues\n // where the reject handlers modify the object so it interferes with the\n // workEmit event payload, wrapping in an Error instance copies the values\n let e = new Error(\n data.error.message,\n data.error.fileName,\n data.error.lineNumber);\n e.stack = data.error.stack;\n e.name = data.error.name;\n \n if (this.ee.listenerCount('reject') > 0) {\n this.ee.emit('reject', e);\n } else {\n // This will happen if the error is thrown during initialization\n throw e;\n }\n\n break;\n default:\n let error = new Error('Received unhandled request from sandbox: ' + data.request + '\\n\\t' + JSON.stringify(data));\n console.error(error);\n break; \n }\n }\n\n async handleRing1Message(data) {\n switch (data.request) {\n case 'applyRequirementsDone':\n // emit internally\n this.ee.emit(data.request, data)\n break;\n default:\n let error = new Error('Received unhandled request from sandbox ring 1: ' + data.request + '\\n\\t' + JSON.stringify(data));\n console.error(error)\n break; \n }\n }\n\n async handleRing2Message(data) {\n debugging('event:ring-2') && debug('event:ring-2', data);\n switch (data.request) {\n case 'dependency': {\n let moduleData;\n try {\n moduleData = await this.supervisorCache.fetchModule(data.data);\n } catch (error) {\n /*\n * In the event of an error here, we want to let the client know there was a problem in\n * loading their module. However, there hasn't yet been an actual slice assigned to the sandbox.\n * Therefore, we assign 'slice 0' to the sandbox, a slice that will never exist, and is used\n * purely for this purpose. \n */\n this.slice = {\n jobAddress: this.jobAddress,\n sliceNumber: 0,\n };\n\n const payload = {\n name: error.name,\n timestamp: error.timestamp,\n message: error.message,\n };\n\n this.emit('workEmit', {\n eventName: 'error',\n payload,\n });\n this.ee.emit('reject', error);\n break;\n }\n this.evaluatorHandle.postMessage({\n request: 'moduleGroup',\n data: moduleData,\n id: data.id,\n });\n break;\n }\n case 'error':\n /*\n * Ring 2 error messages will only fire for problems inside of the worker that are separate from\n * the work function. In most cases there are other handlers for situations where 'error' may be emitted\n * such as timeouts if the expected message isn't recieved. Thus, we will output the error, but nothing else.\n */\n console.error(data.error);\n break;\n case 'describe':\n case 'evalResult':\n case 'resetStateDone':\n case 'assigned':\n // emit internally\n this.ee.emit(data.request, data);\n break;\n case 'reject':\n // emit internally\n this.ee.emit(data.request, data.error);\n break;\n default: {\n const error = new Error(\n `Received unhandled request from sandbox ring 2. Data: ${JSON.stringify(\n data,\n null,\n 2,\n )}`,\n );\n console.error(error);\n break;\n }\n }\n }\n\n async handleRing3Message(data) {\n switch (data.request) {\n case 'complete':\n clearTimeout(this.progressTimeout);\n clearTimeout(this.sliceTimeout);\n this.progressTimeout = this.sliceTimeout = null;\n\n if (this.progress === null) {\n if (this.options.ignoreNoProgress) {\n console.warn(\"ENOPROGRESS silenced by localExec: Progress was not called during this slice's execution, in a remote sandbox this would cause the slice to be failed\");\n } else {\n // If a progress update was never received (progress === null) then reject\n this.ee.emit('reject', new NoProgressError('Sandbox never emitted a progress event.'));\n break;\n }\n }\n this.evaluatorHandle.postMessage({ request: 'resetAndGetCPUTime' })\n this.progress = 100;\n this.completeData = data;\n // The timing report and resolve will be emitted when the CPU time is received. \n break;\n case 'progress':\n let { progress, indeterminate, throttledReports, value } = data;\n this.progress = progress;\n const progressReport = {\n timestamp: Date.now() - this.sliceStartTime,\n progress,\n value,\n throttledReports,\n }\n this.progressReports.last = progressReport;\n if (!indeterminate) {\n this.progressReports.lastDeterministic = progressReport;\n }\n\n this.resetProgressTimeout();\n\n this.emit('sliceProgress', data);\n break;\n\n case 'noProgress':\n let { message } = data;\n\n this.ee.emit('reject', new NoProgressError(message));\n break;\n case 'console':\n this.emit('workEmit', {\n eventName: 'console',\n payload: data.payload\n });\n break;\n\n case 'emitEvent':/* ad-hoc event from the sandbox (work.emit) */\n this.emit('workEmit', {\n eventName: 'custom',\n payload: data.payload\n })\n break;\n case 'measurement':\n this.updateTime(data);\n break;\n case 'workError': {\n this.emit('workEmit', {\n eventName: 'error',\n payload: data.error,\n });\n\n // Warning: rejecting here with just .data.error causes issues\n // where the reject handlers modify the object so it interferes with the\n // workEmit payload, wrapping in an Error instance copies the values\n const wrappedError = new UncaughtExceptionError(\n data.error.message,\n data.error.fileName,\n data.error.lineNumber,\n );\n wrappedError.stack = data.error.stack;\n wrappedError.name = data.error.name;\n\n if (this.ee.listenerCount('reject') > 0) {\n this.ee.emit('reject', wrappedError);\n } else {\n // This will happen if the error is thrown during initialization\n throw wrappedError;\n }\n break;\n }\n default:\n let error = new Error('Received unhandled request from sandbox ring 3: ' + data.request + '\\n\\t' + JSON.stringify(data));\n console.error(error)\n break; \n }\n }\n\n /**\n * Handles progress and completion events from sandbox.\n * Unless explicitly returned out of this function will re-emit the event\n * on @this.ee where the name of the event is event.data.request.\n *\n * @param {object} event - event received from the sandbox\n */\n async onmessage(event) {\n debugging('event') && debug('event', event);\n if (Sandbox.debugEvents) {\n console.debug('sandbox - eventDebug:', {\n id: this.id,\n state: this.state,\n event: JSON.stringify(event)\n })\n }\n\n const { data } = event;\n const ringLevel = data.ringSource\n\n // Give the data to a handler depending on ring level\n if (ringLevel === -1) {\n console.error('Message sent directly from raw postMessage. Terminating worker...');\n console.debug(event);\n return this.terminate(true);\n } else {\n const handler = this.ringMessageHandlers[ringLevel];\n if (handler) {\n handler.call(this, data.value);\n } else {\n console.warn(`No handler defined for message from ring ${ringLevel}`);\n console.debug(event);\n }\n }\n }\n\n /**\n * Error handler for the internal sandbox.\n * Currently just logs the errors that the sandbox spits out.\n */\n onerror(event) {\n console.error('Sandbox emitted an error:', event);\n this.terminate(true, true);\n }\n\n /**\n * Clears the timeout and terminates the sandbox.\n *\n * @param {boolean} [reject = true] - if true call reject\n * @param {boolean} [immediate = false] - passed to terminate, used by standaloneWorker to immediately close the connection\n */\n terminate (reject = true, immediate = false) {\n this.state = TERMINATED;\n \n clearTimeout(this.progressTimeout);\n clearTimeout(this.sliceTimeout);\n this.progressTimeout = this.sliceTimeout = null;\n \n if (this.evaluatorHandle && typeof this.evaluatorHandle.terminate === 'function') {\n try {\n this.evaluatorHandle.terminate(immediate);\n this.evaluatorHandle = null;\n } catch (e) {\n console.error(\"Sandbox.terminate: Encountered an error while terminating the sandbox:\", e);\n } finally {\n this.emit('terminate', this);\n }\n }\n\n if (reject) {\n this.ee.emit('reject', new Error(`Sandbox was terminated or timed out.`));\n }\n\n 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:///./src/dcp-client/worker/sandbox.js?");
|
|
5354
|
+
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\n /** @type {Slice} */\n this.slice = null;\n\n /** @type {number?} */\n this.started = null;\n /** @type {number?} */\n this.sliceStartTime = null;\n /** @type {boolean} */\n this.requiresGPU = false;\n /** @type {string|URL} */\n this.packageURL = dcpConfig.packageManager.location\n /** @type {number?} */\n this.id = Sandbox.getNewId();\n\n this.ringMessageHandlers = [\n this.handleRing0Message,\n this.handleRing1Message,\n this.handleRing2Message,\n this.handleRing3Message,\n ];\n\n this.resetSliceTimeReport();\n }\n\n static getNewId() {\n return Sandbox.idCounter++;\n }\n\n get state () {\n return this._state\n }\n\n set state (value) {\n if (Sandbox.debugState) {\n console.debug(`sandbox - changing state of ${this.id}... ${this._state} -> ${value}`)\n }\n\n if (this.state === TERMINATED && value !== TERMINATED) {\n // For safety!\n throw new Error(`Sandbox set state violation, attepted to change state from ${this.state} to ${value}`);\n }\n\n this._state = value;\n }\n\n get isReadyForAssign () {\n return this.state === READY_FOR_ASSIGN;\n }\n\n get isAssigned () {\n return this.state === ASSIGNED;\n }\n\n get isWorking () {\n return this.state === WORKING;\n }\n\n get isTerminated () {\n return this.state === TERMINATED;\n }\n\n changeWorkingToAssigned () {\n if (this.isWorking)\n this.state = ASSIGNED;\n }\n\n setIsAssigning () {\n this.state = ASSIGNING;\n }\n\n /**\n * Readies the sandbox. This will result in the sandbox being ready and not assigned,\n * it will need to be assigned with a job before it is able to do work.\n *\n * @todo maybe preload specific modules or let the cache pass in what modules to load?\n * @throws on failure to ready\n */\n async start(delay = 0) {\n this.started = Date.now();\n this.state = READYING;\n\n if (delay > 0) await new Promise((resolve) => setTimeout(resolve, delay * timeDilation));\n\n try {\n // RING 0\n this.evaluatorHandle = new this.options.SandboxConstructor({\n name: `DCP Sandbox #${this.id}`,\n });\n this.evaluatorHandle.onerror = this.onerror.bind(this);\n\n const messageHandler = this.onmessage.bind(this);\n this.evaluatorHandle.onmessage = function onmessage(event)\n {\n let data;\n if (event.data.serialized)\n {\n data = kvin.parse(event.data.message);\n }\n else\n {\n data = kvin.unmarshal(event.data);\n }\n messageHandler({ data });\n }\n\n const evaluatorPostMessage = this.evaluatorHandle.postMessage.bind(this.evaluatorHandle);\n this.evaluatorHandle.postMessage = function postMessage(message)\n {\n evaluatorPostMessage(scopedKvin.marshal(message));\n }\n\n const ceci = this;\n this.evaluatorHandle.addEventListener('end', () => ceci.terminate(true));\n\n // Now in RING 1\n\n // Now in RING 2\n await this.describe();\n this.state = READY_FOR_ASSIGN;\n this.emit('ready', this);\n } catch (error) {\n console.warn('Failed to start the sandbox -', error.message);\n this.terminate(false);\n throw error;\n }\n }\n\n /**\n * This will assign the sandbox with a job, loading its sandbox code\n * into the sandbox.\n *\n * @param {string} jobAddress The address of the job to assign to\n * @throws on initialization failure\n */\n async assign(jobAddress) {\n this.jobAddress = jobAddress;\n this.job = await this.supervisorCache.fetchJob(jobAddress, this.allowedOrigins);\n\n assertEq3(this.job.address, jobAddress);\n assert(typeof this.job === 'object');\n assert(typeof this.job.requirements === 'object');\n assert(Array.isArray(this.job.dependencies));\n assert(Array.isArray(this.job.requirePath));\n\n // Extract public data from job, with defaults\n this.public = Object.assign({\n name: `Anonymous Job ${this.job.address.slice(0, 6)}`,\n description: 'Discreetly helping make the world smarter.',\n link: 'https://distributed.computer/about',\n }, this.job.public);\n\n // Future: We may want other filename tags for appliances // RR Nov 2019\n\n // Important: The order of applying requirements before loading the sandbox code\n // is important for modules and sandbox code to set globals over the whitelist.\n await this.applySandboxRequirements(this.job.requirements);\n await this.assignEvaluator();\n this.state = ASSIGNED;\n }\n\n async assignEvaluator() {\n debug('Begin assigning job to evaluator');\n const ceci = this;\n\n return new Promise(function sandbox$$assignEvaluatorPromise(resolve, reject) {\n const message = {\n request: 'assign',\n job: ceci.job,\n sandboxConfig: {\n worker: dcpConfig.worker,\n },\n };\n\n /* 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 = ceci.supervisorCache.cache.job[ceci.jobAddress].mro;\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 // Ceci is the reject callback for when the slice throws an error\n ceci.terminate(false);\n\n ceci.emit('error', err, 'slice');\n\n if (err instanceof NoProgressError) {\n ceci.emit('workEmit', {\n eventName: 'noProgress',\n payload: {\n timestamp: Date.now() - ceci.sliceStartTime,\n data: ceci.slice.datumUri,\n progressReports: ceci.progressReports,\n }\n });\n }\n throw err;\n })\n .finally(function sandbox$$work$finally() {\n ceci.emit('end', sliceHnd);\n });\n }\n\n resetProgressTimeout() {\n if (this.progressTimeout) {\n clearTimeout(this.progressTimeout);\n this.progressTimeout = null;\n }\n\n this.progressTimeout = setTimeout(() => {\n if (this.options.ignoreNoProgress) {\n return console.warn(\"ENOPROGRESS silenced by localExec: In a remote worker, this slice would be stopped for not calling progress frequently enough.\");\n }\n\n this.ee.emit('reject', new NoProgressError(`No progress event was received in the last ${dcpConfig.worker.sandbox.progressTimeout / 1000} seconds.`));\n }, +dcpConfig.worker.sandbox.progressTimeout * timeDilation);\n }\n\n resetSliceTimeout() {\n if (this.sliceTimeout) clearTimeout(this.sliceTimeout);\n\n this.sliceTimeout = setTimeout(() => {\n if (Sandbox.debugWork) return console.warn(\"Sandbox.debugWork: Ignoring slice timeout\");\n\n this.ee.emit('reject', new SliceTooSlowError(`Slice took longer than ${dcpConfig.worker.sandbox.sliceTimeout / 1000} seconds.`));\n }, +dcpConfig.worker.sandbox.sliceTimeout * timeDilation);\n }\n \n async handleRing0Message(data) {\n debugging('event:ring-0') && debug('event:ring-0', data);\n //handling a true ring 0 message\n switch (data.request) {\n case 'sandboxLoaded':\n // emit externally\n this.emit('sandboxLoaded', this)\n break;\n\n case 'scriptLoaded':\n // emit externally\n this.emit('scriptLoaded', data);\n \n if(data.result !== \"success\") {\n this.onerror(data);\n }\n break;\n \n case 'clearTimersDone':\n this.ee.emit(data.request, data);\n break;\n case 'totalCPUTime':\n this.updateTime(data);\n this.completeData.timeReport = this.sliceTimeReport\n this.ee.emit('resolve', this.completeData);\n delete this.completeData;\n break;\n case 'error':\n // Warning: rejecting here with just event.data.error causes issues\n // where the reject handlers modify the object so it interferes with the\n // workEmit event payload, wrapping in an Error instance copies the values\n let e = new Error(\n data.error.message,\n data.error.fileName,\n data.error.lineNumber);\n e.stack = data.error.stack;\n e.name = data.error.name;\n \n if (this.ee.listenerCount('reject') > 0) {\n this.ee.emit('reject', e);\n } else {\n // This will happen if the error is thrown during initialization\n throw e;\n }\n\n break;\n default:\n let error = new Error('Received unhandled request from sandbox: ' + data.request + '\\n\\t' + JSON.stringify(data));\n console.error(error);\n break; \n }\n }\n\n async handleRing1Message(data) {\n switch (data.request) {\n case 'applyRequirementsDone':\n // emit internally\n this.ee.emit(data.request, data)\n break;\n default:\n let error = new Error('Received unhandled request from sandbox ring 1: ' + data.request + '\\n\\t' + JSON.stringify(data));\n console.error(error)\n break; \n }\n }\n\n async handleRing2Message(data) {\n debugging('event:ring-2') && debug('event:ring-2', data);\n switch (data.request) {\n case 'dependency': {\n let moduleData;\n try {\n moduleData = await this.supervisorCache.fetchModule(data.data);\n } catch (error) {\n /*\n * In the event of an error here, we want to let the client know there was a problem in\n * loading their module. However, there hasn't yet been an actual slice assigned to the sandbox.\n * Therefore, we assign 'slice 0' to the sandbox, a slice that will never exist, and is used\n * purely for this purpose. \n */\n this.slice = {\n jobAddress: this.jobAddress,\n sliceNumber: 0,\n };\n\n const payload = {\n name: error.name,\n timestamp: error.timestamp,\n message: error.message,\n };\n\n this.emit('workEmit', {\n eventName: 'error',\n payload,\n });\n this.ee.emit('reject', error);\n break;\n }\n this.evaluatorHandle.postMessage({\n request: 'moduleGroup',\n data: moduleData,\n id: data.id,\n });\n break;\n }\n case 'error':\n /*\n * Ring 2 error messages will only fire for problems inside of the worker that are separate from\n * the work function. In most cases there are other handlers for situations where 'error' may be emitted\n * such as timeouts if the expected message isn't recieved. Thus, we will output the error, but nothing else.\n */\n console.error(data.error);\n break;\n case 'describe':\n case 'evalResult':\n case 'resetStateDone':\n case 'assigned':\n // emit internally\n this.ee.emit(data.request, data);\n break;\n case 'reject':\n // emit internally\n this.ee.emit(data.request, data.error);\n break;\n default: {\n const error = new Error(\n `Received unhandled request from sandbox ring 2. Data: ${JSON.stringify(\n data,\n null,\n 2,\n )}`,\n );\n console.error(error);\n break;\n }\n }\n }\n\n async handleRing3Message(data) {\n switch (data.request) {\n case 'complete':\n clearTimeout(this.progressTimeout);\n clearTimeout(this.sliceTimeout);\n this.progressTimeout = this.sliceTimeout = null;\n\n if (this.progress === null) {\n if (this.options.ignoreNoProgress) {\n console.warn(\"ENOPROGRESS silenced by localExec: Progress was not called during this slice's execution, in a remote sandbox this would cause the slice to be failed\");\n } else {\n // If a progress update was never received (progress === null) then reject\n this.ee.emit('reject', new NoProgressError('Sandbox never emitted a progress event.'));\n break;\n }\n }\n this.evaluatorHandle.postMessage({ request: 'resetAndGetCPUTime' })\n this.progress = 100;\n this.completeData = data;\n // The timing report and resolve will be emitted when the CPU time is received. \n break;\n case 'progress':\n let { progress, indeterminate, throttledReports, value } = data;\n this.progress = progress;\n const progressReport = {\n timestamp: Date.now() - this.sliceStartTime,\n progress,\n value,\n throttledReports,\n }\n this.progressReports.last = progressReport;\n if (!indeterminate) {\n this.progressReports.lastDeterministic = progressReport;\n }\n\n this.resetProgressTimeout();\n\n this.emit('sliceProgress', data);\n break;\n\n case 'noProgress':\n let { message } = data;\n\n this.ee.emit('reject', new NoProgressError(message));\n break;\n case 'console':\n this.emit('workEmit', {\n eventName: 'console',\n payload: data.payload\n });\n break;\n\n case 'emitEvent':/* ad-hoc event from the sandbox (work.emit) */\n this.emit('workEmit', {\n eventName: 'custom',\n payload: data.payload\n })\n break;\n case 'measurement':\n this.updateTime(data);\n break;\n case '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:///./src/dcp-client/worker/sandbox.js?");
|
|
5355
5355
|
|
|
5356
5356
|
/***/ }),
|
|
5357
5357
|
|
|
@@ -5374,7 +5374,7 @@ eval("/**\n * @file worker/slice.js\n *\n * A wrapper for the slice object retur
|
|
|
5374
5374
|
/*! no static exports found */
|
|
5375
5375
|
/***/ (function(module, exports, __webpack_require__) {
|
|
5376
5376
|
|
|
5377
|
-
eval("/**\n * @file worker/supervisor-cache.js\n *\n * A cache for the supervisor, anything the supervisor\n * may request that is cacheable (the same every time its requested)\n * can be cached in this class.\n *\n * Currently only jobs and modules are cached.\n *\n *
|
|
5377
|
+
eval("/**\n * @file worker/supervisor-cache.js\n *\n * A cache for the supervisor, anything the supervisor\n * may request that is cacheable (the same every time its requested)\n * can be cached in this class.\n *\n * Currently only jobs and modules are cached.\n *\n * @author Matthew Palma, mpalma@kingsds.network\n * @date May 2019\n */\n\n/* global dcpConfig */\n\nconst debugging = __webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope('worker');\nconst { justFetch, fetchURI, dumpObject } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\nconst { EventEmitter } = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\");\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\n\nclass SupervisorCache extends EventEmitter {\n constructor (supervisor) {\n \n super('SupervisorCache')\n this.supervisor = supervisor;\n this.cache = {\n job: {},\n module: {},\n dependency: {},\n }\n \n this.promises = {\n job: {},\n module: {},\n dependency: {},\n }\n\n this.lastAccess = {}\n for (let key in this.cache) {\n this.lastAccess[key] = {}\n }\n }\n\n /**\n * Returns an object listing what jobs and modules are currently cached.\n * @returns {object} - in the form: { job: [0xgen1, 0xgen2,...], modules: [modGroup1, modGroup2,...] }\n */\n get cacheDescription () {\n let description = {}\n for (let key in this.cache) {\n description[key] = Object.keys(this.cache[key])\n }\n return description\n }\n\n /**\n * Returns an array of all jobId currently cached\n * @returns {Array} all the jobId's in the cache\n */\n get jobs () {\n return Object.keys(this.cache.job);\n }\n\n /**\n * Attempts to look up an item from the cache.\n * If item is found its last access time is updated.\n *\n * @param {string} key - the cache to look in (job or module)\n * @param {string} id - the items identifier (jobAddress or module group name)\n *\n * @returns {any} The value of the stored item or null if nothing is found\n */\n fetch(key, id) {\n if (typeof this.cache[key] === 'undefined') {\n throw new Error(`${key} does not relate to any cache.`);\n }\n if (this.cache[key][id]) {\n this.lastAccess[key][id] = Date.now();\n return this.cache[key][id];\n }\n return null;\n }\n\n /**\n * Stores a fetched value for one of the caches.\n *\n * @param {string} key - the cache to store the item in\n * @param {string} id - the items identifier (job Address or module group name)\n * @param {any} value - the item to store\n *\n * @returns {any} - @value passed in\n */\n store (key, id, value) {\n if (typeof this.cache[key] === 'undefined') {\n throw new Error(`${key} does not relate to any cache.`)\n }\n this.cache[key][id] = value\n this.lastAccess[key][id] = Date.now()\n return value\n }\n\n /**\n * Removes a job or module group from the cache.\n *\n * @param {string} key - the cache to remove the item from (jobs or module groups)\n * @param {string} id - the items identifier (job Address or module group name)\n *\n * @returns {string} - @id passed in\n */\n remove (key, id) {\n if (typeof this.cache[key] === 'undefined') {\n throw new Error(`${key} does not relate to any cache.`)\n }\n delete this.cache[key][id];\n delete this.lastAccess[key][id];\n return id;\n }\n\n /** \n * Fetch a job from the job cache. If the job has components which are \n * which need to be fetched over the network, they are fetched before the\n * returned promise is resolved.\n *\n * The job cache is initially populated during fetchTask.\n */\n async fetchJob(address, allowedOrigins) {\n let job = this.fetch('job', address);\n\n if (!job) {\n let e = new Error(`No job in supervisor cache with address ${address}`);\n e.code = 'ENOJOB';\n throw e;\n }\n\n // XXXpfr Excellent tracing.\n if (debugging('worker')) {\n dumpObject(job, 'SupervisorCache.fetchJob: job', 128);\n }\n\n if (!job.workFunction) {\n job.workFunction = await fetchURI(job.codeLocation, allowedOrigins, dcpConfig.worker.allowOrigins.fetchWorkFunctions);\n delete job.codeLocation;\n }\n if (!job.arguments) {\n let promises = [];\n let uris = job.argumentsLocation;\n if (uris)\n for (let i = 0; i < uris.length; i++)\n promises.push(fetchURI(uris[i].value, allowedOrigins, dcpConfig.worker.allowOrigins.fetchArguments));\n\n job.arguments = await Promise.all(promises);\n delete job.argumentsLocation;\n }\n // if job input data is range object, we send the range object URI to the worker\n if (!job.mro && job.MROLocation) {\n job.mro = await fetchURI(job.MROLocation, allowedOrigins, dcpConfig.worker.allowOrigins.fetchWorkFunctions);\n delete job.MROLocation;\n }\n \n return job;\n }\n\n /**\n * Attempts to fetch a module group from the cache and\n * if it's not found it attempts to fetch then store\n * the module group from the package manager.\n *\n * @param {array} modulesArray - the array of modules requested \n * - (when stringified it's the identifier of the module group)\n *\n * @returns {Promise<object>} - the module group\n * @throws when the module group can not be fetched\n */\n async fetchModule(modulesArray) {\n const cacheKey = JSON.stringify(modulesArray);\n let modules = this.fetch('module', cacheKey);\n if (modules !== null) {\n return modules;\n }\n\n if (this.promises.module[cacheKey]) {\n return this.promises.module[cacheKey];\n }\n\n const {\n success,\n payload: responsePayload,\n } = await this.supervisor.packageManagerConnection.send('fetchModule', {\n modules: modulesArray,\n });\n\n if (!success) {\n /**\n * Preserving the error message by not rewrapping it with DCPError incase\n * we want to let clients know which module couldn't be fetched.\n */\n throw responsePayload;\n }\n\n this.promises.module[cacheKey] = responsePayload;\n modules = await this.promises.module[cacheKey];\n delete this.promises.module[cacheKey];\n return this.store('module', cacheKey, modules);\n }\n\n /**\n * Attempts to fetch a dependency from the cache and\n * if it's not found it attempts to fetch then store\n * the dependency from the package manager.\n *\n * @param {string} dependencyUri - The URI of the dependency\n *\n * @returns {Promise<string>} file contents\n * @throws when the dependency can not be fetched\n */\n async fetchDependency(dependencyUri) {\n let dependency = this.fetch('dependency', dependencyUri);\n if (dependency !== null) {\n return dependency;\n }\n\n if (this.promises.dependency[dependencyUri]) {\n return this.promises.dependency[dependencyUri];\n }\n\n const url = dcpConfig.packageManager.location.resolve(dependencyUri);\n this.promises.dependency[dependencyUri] = justFetch(url, 'string', 'GET', true);\n\n dependency = await this.promises.dependency[dependencyUri];\n\n delete this.promises.dependency[dependencyUri];\n\n return this.store('dependency', dependencyUri, dependency);\n }\n}\n\nexports.SupervisorCache = SupervisorCache;\n\n\n//# sourceURL=webpack:///./src/dcp-client/worker/supervisor-cache.js?");
|
|
5378
5378
|
|
|
5379
5379
|
/***/ }),
|
|
5380
5380
|
|
|
@@ -5386,7 +5386,7 @@ eval("/**\n * @file worker/supervisor-cache.js\n *\n * A cache for the superviso
|
|
|
5386
5386
|
/***/ (function(module, exports, __webpack_require__) {
|
|
5387
5387
|
|
|
5388
5388
|
"use strict";
|
|
5389
|
-
eval("/**\n * @file worker/supervisor.js\n *\n * The component that controls each of the sandboxes\n * and distributes work to them. Also communicates with the\n * scheduler to fetch said work.\n *\n * The supervisor readies sandboxes before/while fetching slices.\n * This means sometimes there are extra instantiated WebWorkers\n * that are idle (in this.readiedSandboxes). Readied sandboxes can\n * be used for any slice. After a readied sandbox is given a slice\n * it becomes assigned to slice's job and can only do work\n * for that job.\n *\n * After a sandbox completes its work, the sandbox becomes cached\n * and can be reused if another slice with a matching job is fetched.\n *\n * @author Matthew Palma, mpalma@kingsds.network\n * Ryan Rossiter, ryan@kingsds.network\n * @date May 2019\n */\n\n/* global dcpConfig */\n// @ts-check\n\n\nconst constants = __webpack_require__(/*! dcp/common/scheduler-constants */ \"./src/common/scheduler-constants.js\");\nconst hash = __webpack_require__(/*! dcp/common/hash */ \"./src/common/hash.js\");\nconst wallet = __webpack_require__(/*! dcp/dcp-client/wallet */ \"./src/dcp-client/wallet/index.js\");\nconst protocolV4 = __webpack_require__(/*! dcp/protocol-v4 */ \"./src/protocol-v4/index.js\");\nconst DCP_ENV = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\n\nconst debugging = __webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope('worker');\nconst { assert } = __webpack_require__(/*! dcp/common/dcp-assert */ \"./src/common/dcp-assert.js\");\nconst { EventEmitter } = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\");\nconst { Sandbox, SandboxError } = __webpack_require__(/*! ./sandbox */ \"./src/dcp-client/worker/sandbox.js\");\nconst { Slice } = __webpack_require__(/*! ./slice */ \"./src/dcp-client/worker/slice.js\");\nconst { SupervisorCache } = __webpack_require__(/*! ./supervisor-cache */ \"./src/dcp-client/worker/supervisor-cache.js\");\nconst { DcpURL } = __webpack_require__(/*! dcp/common/dcp-url */ \"./src/common/dcp-url.js\");\nconst { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\nconst { localStorage } = __webpack_require__(/*! dcp/common/dcp-localstorage */ \"./src/common/dcp-localstorage.js\");\nconst { booley, encodeDataURI, makeDataURI, leafMerge, asleepMs, shortTime, justFetch,\n dumpSandboxes, dumpSlices, dumpSandboxesIfNotUnique, dumpSlicesIfNotUnique, 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 debug = (...args) => {\n if (debugging('supervisor')) {\n console.debug('dcp-client:worker:supervisor', ...args);\n }\n};\n\nconst supervisorTuning = dcpConfig.future('supervisor.tuning');\nconst tuning = {\n watchdogInterval: 7, /**< seconds - time between fetches when ENOTASK(? /wg nov 2019) */\n minSandboxStartDelay: 0.1, /**< seconds - minimum time between WebWorker starts */\n maxSandboxStartDelay: 0.7, /**< seconds - maximum delay time between WebWorker starts */\n ...supervisorTuning\n};\n\n/** Make timers 10x slower when running in niim */\nlet timeDilation = 1;\nif (DCP_ENV.platform === 'nodejs') {\n /** Make timers 10x slower when running in niim */\n timeDilation = (requireNative('module')._cache.niim instanceof requireNative('module').Module) ? 10 : 1;\n}\n\ndcpConfig.future('worker.sandbox', { progressReportInterval: (5 * 60 * 1000) });\nconst sandboxTuning = dcpConfig.worker.sandbox;\n\n/**\n * @typedef {*} opaqueId\n */\n\n/**\n * @typedef {object} SandboxSlice\n * @property {Sandbox} sandbox\n * @property {Slice} slice\n */\n\n/**\n * @typedef {object} Signature\n * @property {Uint8Array} r\n * @property {Uint8Array} s\n * @property {Uint8Array} v\n */\n\n/**\n * @typedef {object} SignedAuthorizationMessageObject\n * @property {object} auth\n * @property {Signature} signature\n * @property {module:dcp/wallet.Address} owner\n */\n\n/** @typedef {import('.').Worker} Worker */\n/** @typedef {import('.').SupervisorOptions} SupervisorOptions */\n\nclass Supervisor extends EventEmitter {\n /**\n * @constructor\n * @param {Worker} worker\n * @param {SupervisorOptions} options\n */\n constructor (worker, options={}) {\n super('Supervisor');\n\n /** @type {Worker} */\n this.worker = worker;\n\n /** @type {Sandbox[]} */\n this.sandboxes = [];\n\n /**\n * XXXpfr @todo We may still needs this.slices to communicate state to result-submitter status endpoint, but it isn't used otherwise.\n * @deprecated\n * @type {Slice[]}\n */\n this.slices = [];\n\n /** @type {Slice[]} */\n this.queuedSlices = [];\n\n /** @type {Sandbox[]} */\n this.readiedSandboxes = [];\n\n /** @type {Sandbox[]} */\n this.assignedSandboxes = [];\n\n /** @type {boolean} */\n this.matching = false;\n\n /** @type {boolean} */\n this.doNotPrune = false;\n\n if (!options) {\n console.error('Supervisor Options', options, new Error().stack);\n options = {};\n }\n \n /** @type {object} */\n this.options = {\n jobAddresses: options.jobAddresses || [/* all jobs unless priorityOnly */],\n ...options,\n };\n\n const { paymentAddress, identityKeystore } = options;\n if (paymentAddress) {\n if (paymentAddress instanceof wallet.Keystore) {\n this.paymentAddress = paymentAddress.address;\n } else {\n this.paymentAddress = new wallet.Address(paymentAddress);\n }\n } else {\n this.paymentAddress = null;\n }\n\n this._identityKeystore = identityKeystore;\n\n this.allowedOrigins = dcpConfig.worker.allowOrigins.any;\n if(options.allowedOrigins && options.allowedOrigins.length > 0)\n this.allowedOrigins = options.allowedOrigins.concat(this.allowedOrigins);\n\n /**\n * Maximum sandboxes allowed to work at a given time.\n * @type {number}\n */\n this.maxWorkingSandboxes = options.maxWorkingSandboxes || 1;\n\n /** @type {number} */\n this.defaultMaxGPUs = 1;\n // this.GPUsAssigned = 0;\n \n // Object.defineProperty(this, 'GPUsAssigned', {\n // get: () => this.workingSandboxes.filter(sb => !!sb.requiresGPU).length,\n // enumerable: true,\n // configurable: false,\n // });\n\n /** @type {boolean} */\n this.isFetchingNewWork = false;\n /**\n * TODO: Remove this when the supervisor sends all of the sandbox\n * capabilities to the scheduler when fetching work.\n * @type {object}\n */\n this.capabilities = null;\n\n // This sets an offset into the watchdog bin at which to fire the sweeper\n /** @deprecated - UNUSED */\n this.watchdogSlotTime = options.watchdogInterval || tuning.watchdogInterval * 1000;\n /** @deprecated - UNUSED */\n this.watchdogOffset = Math.random();\n /** @deprecated - UNUSED */\n this.watchdogTimeout = null;\n\n /** @type {number} */\n this.lastProgressReport = 0;\n\n // @hack - dcp-env.isBrowserPlatform is not set unless the platform is _explicitly_ set,\n // using the default detected platform doesn't set it.\n // Fixing that causes an error in the wallet module's startup on web platform, which I\n // probably can't fix in a reasonable time this morning.\n // ~ER2020-02-20\n\n if (!options.maxWorkingSandboxes\n && DCP_ENV.browserPlatformList.includes(DCP_ENV.platform)\n && navigator.hardwareConcurrency > 1) {\n this.maxWorkingSandboxes = navigator.hardwareConcurrency - 1;\n if (typeof navigator.userAgent === 'string') {\n if (/(Android).*(Chrome|Chromium)/.exec(navigator.userAgent)) {\n this.maxWorkingSandboxes = 1;\n console.log('Doing work with Chromimum browsers on Android is currently limited to one sandbox');\n }\n }\n }\n\n /** @type {SupervisorCache} */\n this.cache = new SupervisorCache(this);\n /** @type {object} */\n this._connections = {}; /* active DCPv4 connections */\n // Call the watchdog every 7 seconds.\n this.watchdogInterval = setInterval(() => this.watchdog(), 7000);\n\n const ceci = this;\n\n // Initialize to null so these properties are recognized for the Supervisor class\n this.taskDistributorConnection = null;\n this.eventRouterConnection = null;\n this.resultSubmitterConnection = null;\n this.packageManagerConnection = null;\n this.openTaskDistributorConn = function openTaskDistributorConn()\n {\n let config = dcpConfig.scheduler.services.taskDistributor;\n ceci.taskDistributorConnection = new protocolV4.Connection(config, ceci.identityKeystore, connectionOptions(config.location, 'taskDistributor'));\n ceci.taskDistributorConnection.on('close', ceci.openTaskDistributorConn);\n }\n\n this.openEventRouterConn = function openEventRouterConn()\n {\n let config = dcpConfig.scheduler.services.eventRouter;\n ceci.eventRouterConnection = new protocolV4.Connection(config, ceci.identityKeystore, connectionOptions(config.location, 'eventRouter'));\n ceci.eventRouterConnection.on('close', ceci.openEventRouterConn);\n }\n \n this.openResultSubmitterConn = function openResultSubmitterConn()\n {\n let config = dcpConfig.scheduler.services.resultSubmitter;\n ceci.resultSubmitterConnection = new protocolV4.Connection(config, ceci.identityKeystore, connectionOptions(config.location, 'resultSubmitter'));\n ceci.resultSubmitterConnection.on('close', ceci.openResultSubmitterConn);\n }\n\n this.openPackageManagerConn = function openPackageManagerConn()\n {\n let config = dcpConfig.packageManager;\n ceci.packageManagerConnection = new protocolV4.Connection(config, ceci.identityKeystore, connectionOptions(config.location, 'packageManager'));\n ceci.packageManagerConnection.on('close', ceci.openPackageManagerConn);\n }\n }\n\n /**\n * Return worker opaqueId.\n * @type {opaqueId}\n */\n get workerOpaqueId() {\n if (!this._workerOpaqueId)\n this._workerOpaqueId = localStorage.getItem('workerOpaqueId');\n\n if (!this._workerOpaqueId || this._workerOpaqueId.length !== 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 this.openTaskDistributorConn();\n this.openEventRouterConn();\n this.openResultSubmitterConn();\n this.openPackageManagerConn();\n }\n\n /** Set the default identity keystore -- needs to happen before anything that talks\n * to the scheduler for work gets called. This is a wart and should be removed by\n * refactoring.\n *\n * The default identity keystore will be used if the Supervisor was not provided\n * with an alternate. This keystore will be located via the Wallet API, and \n * if not found, a randomized default identity will be generated. \n *\n * @param {object} ks An instance of wallet::Keystore -- if undefined, we pick the best default we can.\n * @returns {Promise<void>}\n */\n async setDefaultIdentityKeystore(ks) {\n try {\n if (ks) {\n this.defaultIdentityKeystore = ks;\n return;\n }\n\n if (this.defaultIdentityKeystore)\n return;\n\n try {\n this.defaultIdentityKeystore = await wallet.getId();\n } catch(e) {\n debugging('supervisor') && console.debug('supervisor: generating default identity', this.defaultIdentityKeystore.address);\n this.defaultIdentityKeystore = await new wallet.IdKeystore(null, '');\n }\n } finally {\n debugging('supervisor') && console.debug('supervisor: set default identity =', this.defaultIdentityKeystore.address);\n }\n }\n \n //\n // What follows is a bunch of utility functions for creating filtered views\n // of the slices and sandboxes array.\n //\n\n /**\n * Sandboxes that are in WORKING state.\n * Warning: Do not rely on this information being 100% accurate -- it may change in the next instant.\n * @type {Sandbox[]}\n */\n get workingSandboxes() {\n return this.sandboxes.filter(sandbox => sandbox.isWorking);\n }\n\n /**\n * This property is used as the target number of sandboxes to be associated with slices and transition sandbox-state from ASSIGNED -> WORKING.\n *\n * It is used in this.watchdog as to prevent a call to this.work when unallocatedSpace <= 0.\n * It is also used in this.distributeQueuedSlices where it is passed as an argument to this.matchSlicesWithSandboxes to indicate how many sandboxes\n * to associate with slices and transition sandbox-state from ASSIGNED -> WORKING.\n *\n * Warning: Do not rely on this information being 100% accurate -- it may change in the next instant.\n * @type {number}\n */\n get unallocatedSpace() {\n return this.maxWorkingSandboxes - this.sandboxes.filter(sandbox => sandbox.allocated).length;\n }\n\n /**\n * Slices that have SLICE_STATUS_UNASSIGNED status.\n * Warning: Do not rely on this information being 100% accurate -- it may change in the next instant.\n * @type {Slice[]}\n */\n get snapshotOfQueuedSlices() {\n return this.slices.filter(slice => slice.isUnassigned);\n }\n\n /**\n * Slices that have SLICE_STATUS_WORKING status.\n * Warning: Do not rely on this information being 100% accurate -- it may change in the next instant.\n * @type {Slice[]}\n */\n get snapshotOfWorkingSlices() {\n return this.slices.filter(slice => slice.isWorking)\n }\n\n /**\n * Sandboxes that are in READY_FOR_ASSIGN state.\n * Warning: Do not rely on this information being 100% accurate -- it may change in the next instant.\n * @type {Sandbox[]}\n */\n get snapshotOfReadiedSandboxes() {\n return this.sandboxes.filter(sandbox => sandbox.isReadyForAssign);\n }\n\n /**\n * Sandboxes that are in ASSIGNED state.\n * Warning: Do not rely on this information being 100% accurate -- it may change in the next instant.\n * @type {Sandbox[]}\n */\n get snapshotOfAssignedSandboxes() {\n return this.sandboxes.filter(sandbox => sandbox.isAssigned);\n }\n\n /**\n * Sandboxes that are in TERMINATED state.\n * Warning: Do not rely on this information being 100% accurate -- it may change in the next instant.\n * @type {Sandbox[]}\n */\n get snapshotOfTerminatedSandboxes() {\n return this.sandboxes.filter(sandbox => sandbox.isTerminated);\n }\n\n /**\n * Remove element from theArray.\n * @parameter {Array<*>} theArray\n * @parameter {object|number} element\n * @parameter {boolean} [assertExists = true]\n */\n removeElement(theArray, element, assertExists = false) {\n let index = theArray.indexOf(element);\n assert(index !== -1 || !assertExists);\n if (index !== -1) theArray.splice(index, 1);\n }\n\n /**\n * Find the authorization message wrt jobAddress and sliceNumber.\n * @parameter {string} text\n * @parameter {opaqueId} jobAddress\n * @parameter {number} sliceNumber\n * @returns {SignedAuthorizationMessageObject}\n */\n findAuthorizationMessage(text, jobAddress, sliceNumber) {\n const slice = this.slices.find(s => s.jobAddress === jobAddress && s.sliceNumber === sliceNumber);\n if (slice && slice.authorizationMessage)\n return slice.authorizationMessage;\n else\n throw new DCPError(`Undefined authorization. ${jobAddress}, ${sliceNumber}`, 'NOAUTH')\n }\n\n /**\n * If the elements of sandboxSliceArray are not unique, log the duplicates and dump the array.\n * @parameter {SandboxSlice[]} sandboxSliceArray\n * @parameter {string} header\n */\n dumpSandboxSlicesIfNotUnique(sandboxSliceArray, header) {\n if (!this.isUniqueSandboxSlices(sandboxSliceArray, header))\n this.dumpSandboxSlices(sandboxSliceArray);\n }\n\n /**\n * Log sandboxSlice.\n * @parameter {SandboxSlice} sandboxSlice\n * @returns {string}\n */\n dumpSandboxSlice(sandboxSlice) {\n return `${sandboxSlice.sandbox.id}-${sandboxSlice.slice.sliceNumber}.${sandboxSlice.slice.jobAddress}`;\n }\n\n /**\n * Log { sandbox, slice }.\n * @parameter {Sandbox} sandbox\n * @parameter {Slice} slice\n * @returns {string}\n */\n dumpSandboxAndSlice(sandbox, slice) {\n return this.dumpSandboxSlice({ sandbox, slice });\n }\n\n /**\n * Dump sandboxSliceArray.\n * @parameter {SandboxSlice[]} sandboxSliceArray\n * @parameter {string} header\n */\n dumpSandboxSlices(sandboxSliceArray, header) {\n if (header) console.log(`\\n${header}`);\n sandboxSliceArray.forEach(x => console.log(`\\t{ ${x.sandbox.id}.${x.sandbox.jobAddress}.${x.sandbox.state}, ${x.slice.sliceNumber}.${x.slice.jobAddress}.${x.slice.status} }`));\n }\n\n /**\n * Check sandboxSliceArray for duplicates.\n * @parameter {SandboxSlice[]} sandboxSliceArray\n * @parameter {string} header\n * @parameter {function} log\n * @returns {boolean}\n */\n isUniqueSandboxSlices(sandboxSliceArray, header, log) {\n return sandboxSliceArray.length === this.makeUniqueSandboxSlices(sandboxSliceArray, header, log).length;\n }\n\n /**\n * Returns a copy of sandboxSliceArray with all duplicates removed.\n * @parameter {SandboxSlice[]} sandboxSliceArray\n * @parameter {string} header\n * @parameter {function} log\n * @returns {SandboxSlice[]}\n */\n makeUniqueSandboxSlices(sandboxSliceArray, header, log) {\n const result = [], slices = [], sandboxes = [];\n let once = true;\n sandboxSliceArray.forEach(x => {\n const sliceIndex = slices.indexOf(x.slice);\n const sandboxIndex = sandboxes.indexOf(x.sandbox);\n\n if (sandboxIndex >= 0) {\n if (once && header) console.log(`\\n${header}`); once = false;\n log ? log(x.sandbox) : console.log(`\\tDANGER: Found duplicate sandbox ${x.sandbox.id}.${x.sandbox.jobAddress}.${x.sandbox.state}.`);\n } else sandboxes.push(x.sandbox);\n\n if (sliceIndex >= 0) {\n if (once && header) console.log(`\\n${header}`); once = false;\n log ? log(x.slice) : console.log(`\\tDANGER: Found duplicate slice ${x.slice.sliceNumber}.${x.slice.jobAddress}.${x.slice.status}.`);\n } else {\n slices.push(x.slice);\n if (sandboxIndex < 0) result.push(x);\n }\n });\n return result;\n }\n\n /** NOT used ATM, it's faster to just filter the assigned sandboxes for a jobAddress on-demand\n * \n * Bins the assigned sandboxes in an object, keyed by their jobAddress.\n * { 0xgenAddress1: sandboxes[], 0xgenAddress2: sandboxes[], ... }\n * @type {object}\n */\n get assignedSandboxesSorted () {\n return this.assignedSandboxes.reduce((o, w) => {\n if (!w.jobAddress) throw new Error(\"Assigned sandbox doesn't have a job opaque id\", w);\n\n if (w.jobAddress in o) {\n o[w.jobAddress].push(w);\n } else o[w.jobAddress] = [w];\n\n return o;\n }, {});\n }\n\n /**\n * Attempts to create and start a given number of sandboxes.\n * The sandboxes that are created can then be assigned for a\n * specific job at a later time. All created sandboxes\n * get put into the @this.readiedSandboxes array.\n *\n * @param {number} numSandboxes - the number of sandboxes to create\n * @returns {Promise<Sandbox[]>} - resolves with array of created sandboxes, rejects otherwise\n * @throws when given a numSandboxes is not a number > 0 or if numSandboxes is Infinity\n */\n async readySandboxes (numSandboxes) {\n debug('Readying sandboxes');\n if (typeof numSandboxes !== 'number' || Number.isNaN(numSandboxes) || numSandboxes === Infinity) {\n throw new Error(`${numSandboxes} is not a number of sandboxes that can be readied.`);\n }\n if (numSandboxes <= 0) {\n return [];\n }\n\n const sandboxStartPromises = [];\n const sandboxes = [];\n const errors = [];\n for (let i = 0; i < numSandboxes; i++) {\n const sandbox = new Sandbox(this.cache, {\n ...this.options.sandboxOptions,\n }, this.allowedOrigins);\n sandbox.addListener('ready', () => this.emit('sandboxReady', sandbox));\n sandbox.addListener('start', () => {\n this.emit('sandboxStart', sandbox);\n\n // When sliceNumber == 0, result-submitter status skips the slice,\n // so don't send it in the first place.\n // The 'start' event is fired when a worker starts up, hence there's no way\n // to determine whether sandbox has a valid slice without checking.\n if (sandbox.slice) {\n const jobAddress = sandbox.jobAddress;\n const sliceNumber = sandbox.slice.sliceNumber;\n const authorizationMessage = this.findAuthorizationMessage('Supervisor.readySandboxes sandboxStart:', jobAddress, sliceNumber);\n\n this.resultSubmitterConnection.send('status', {\n worker: this.workerOpaqueId,\n slices: [{\n job: jobAddress,\n sliceNumber: sliceNumber,\n status: 'begin',\n authorizationMessage,\n }],\n });\n }\n });\n sandbox.addListener('workEmit', ({ eventName, payload }) => {\n // Need to check if the sandbox hasn't been assigned a slice yet.\n if (!sandbox.slice) {\n if (Supervisor.debugBuild) {\n console.error(\n `Sandbox not assigned a slice before sending workEmit message to scheduler. 'workEmit' event originates from \"${eventName}\" event`, \n payload,\n );\n }\n } else { // Need else otherwise assert fires.\n\n const jobAddress = sandbox.slice.jobAddress;\n const sliceNumber = sandbox.slice.sliceNumber;\n // sliceNumber can be zero if it came from a problem with loading modules.\n assert(jobAddress && (sliceNumber || sliceNumber === 0));\n try {\n /* Make sure we actually have an authorization message at jobAddress and sliceNumber. */\n const authorizationMessage = this.findAuthorizationMessage('Supervisor.readySandboxes workEmit:', jobAddress, sliceNumber);\n\n /* DCP-1698 Send auth msg with tasks to worker, then validate authority of worker to send slice info back to scheduler. */\n this.eventRouterConnection.send('workEmit', {\n eventName,\n payload,\n job: jobAddress,\n slice: sliceNumber,\n worker: this.workerOpaqueId,\n authorizationMessage,\n });\n } catch (error) {\n debugging('supervisor') &&\n console.debug(\n `Authorizing failed: jobAddress: ${jobAddress}, slice: ${sliceNumber}, error: ${error}`,\n );\n }\n }\n });\n\n // When any sbx completes, \n sandbox.addListener('complete', () => {\n // this.distributeQueuedSlices();\n this.watchdog();\n //this.work();\n });\n // 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.map(sandbox => sandbox.isTerminated);\n terminatedSandboxes = terminatedSandboxes.filter(s => s === true);\n if(terminatedSandboxes.length === this.sandboxes.length) {\n await this.readySandboxes(1);\n \n if(this.sandboxes.length !== terminatedSandboxes.length + 1) // if cannot create a new sandbox\n {\n this.returnSlices(this.queuedSlices).then(() => {\n // Kill corresponding entries in this.slices.\n this.queuedSlices.forEach(slice => {\n this.removeElement(this.slices, slice);\n });\n this.queuedSlices.length = 0;\n });\n }\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 this.sandboxes.push(sandbox);\n sandboxes.push(sandbox);\n })\n .catch((err) => {\n errors.push(err);\n this.returnSandbox(sandbox);\n if(err.code === 'ENOWORKER'){\n throw new DCPError(\"Cannot use localExec without dcp-worker installed. Use the command 'npm install dcp-worker' to install the neccessary modules.\", 'ENOWORKER');\n }\n }));\n }\n\n await Promise.all(sandboxStartPromises);\n\n if (errors.length) {\n console.warn(`Failed to ready ${errors.length} of ${numSandboxes} sandboxes.`, errors);\n throw new Error('Failed to ready sandboxes.');\n }\n\n if (this.readiedSandboxes.length > 0)\n sandboxes.push(...this.readiedSandboxes);\n this.readiedSandboxes = sandboxes;\n\n debugging() && console.log(`readySandboxes: all readied sandboxes ${JSON.stringify(this.readiedSandboxes.map(sandbox => sandbox.id))}`);\n \n return sandboxes;\n }\n\n /**\n * Accepts a sandbox after it has finished working or encounters an error.\n * If the sandbox was terminated or if \"!slice || slice.failed\" then\n * the sandbox will be removed from the sandboxes array and terminated if necessary.\n * Otherwise it will try to distribute a slice to the sandbox immediately.\n *\n * @param {Sandbox} sandbox - the sandbox to return\n * @param {Slice} [slice] - the slice just worked on; !slice => terminate\n */\n returnSandbox (sandbox, slice) {\n if (!slice || slice.failed || sandbox.isTerminated) {\n this.removeElement(this.sandboxes, sandbox);\n if (!sandbox.isTerminated) {\n sandbox.terminate(false);\n }\n }\n }\n\n /**\n * Terminates sandboxes, in order of creation, when the total started sandboxes exceeds the total allowed sandboxes.\n *\n * @todo Check through the currently started sandboxes and attempt to terminate\n * ones that aren't frequently used.\n * ^^^ This todo may be partially done due to terminating older sandboxes - RR Nov 2019\n * @returns {Promise<void>}\n */\n pruneSandboxes () {\n let numOver = this.sandboxes.length - (dcpConfig.worker.maxAllowedSandboxes + this.maxWorkingSandboxes);\n if (numOver <= 0) return;\n \n // Don't kill readied sandboxes while creating readied sandboxes.\n if (!this.doNotPrune) {\n for (let index = 0; index < this.readiedSandboxes.length; ) {\n const sandbox = this.readiedSandboxes[index];\n // If the sandbox is allocated, advance to the next one in the list.\n if (sandbox.allocated) {\n index++;\n continue;\n }\n // Otherwise, remove this sandbox but look at the same array index in the next loop.\n debugging() && console.log(`pruneSandboxes: Killing readied sandbox ${sandbox.id}`);\n this.readiedSandboxes.splice(index, 1);\n this.returnSandbox(sandbox);\n\n if (--numOver <= 0) break;\n }\n }\n\n if (numOver <= 0) return;\n for (let index = 0; index < this.assignedSandboxes.length; ) {\n const sandbox = this.assignedSandboxes[index];\n // If the sandbox is allocated, advance to the next one in the list.\n if (sandbox.allocated) {\n index++;\n continue;\n }\n // Otherwise, remove this sandbox but look at the same array index in the next loop.\n debugging() && console.log(`pruneSandboxes: Killing assigned sandbox ${sandbox.id}`);\n this.assignedSandboxes.splice(index, 1);\n this.returnSandbox(sandbox);\n\n if (--numOver <= 0) break;\n }\n }\n \n /**\n * Basic watch dog to check if there are idle sandboxes and\n * attempts to nudge the supervisor to feed them work.\n *\n * Run in an interval created in @constructor .\n * @returns {Promise<void>}\n */\n async watchdog () {\n if (!this.watchdogState)\n this.watchdogState = {};\n \n // Every 5 minutes, report progress of all working slices to the scheduler\n if (Date.now() > ((this.lastProgressReport || 0) + sandboxTuning.progressReportInterval)) {\n // console.log('454: Assembling progress update...');\n this.lastProgressReport = Date.now();\n \n const slices = [];\n \n this.queuedSlices.forEach(slice => {\n const jobAddress = slice.jobAddress;\n const sliceNumber = slice.sliceNumber;\n const authorizationMessage = this.findAuthorizationMessage('Supervisor.watchdog:', jobAddress, sliceNumber);\n\n if (authorizationMessage) {\n addToSlicePayload(slices, jobAddress, sliceNumber, sliceStatus.scheduled, authorizationMessage);\n }\n })\n \n this.workingSandboxes.forEach(sb => {\n // The lifetime of sandbox.isWorking is (or should be) precisely defined,\n // so we can assume sb.slice is always valid here.\n assert(sb.slice);\n\n const jobAddress = sb.jobAddress;\n const sliceNumber = sb.slice.sliceNumber;\n const authorizationMessage = this.findAuthorizationMessage('Supervisor.watchdog:', jobAddress, sliceNumber);\n\n if (authorizationMessage) {\n addToSlicePayload(slices, jobAddress, sliceNumber, 'progress', authorizationMessage);\n }\n });\n\n if (slices.length) {\n // console.log('471: sending progress update...');\n const progressReportPayload = {\n worker: this.workerOpaqueId,\n slices,\n };\n\n this.resultSubmitterConnection.send('status', progressReportPayload)\n .catch(error => {\n console.error('479: Failed to send status update:', error/*.message*/);\n });\n }\n }\n\n if (this.worker.working) {\n if (this.unallocatedSpace > 0) {\n await this.work().catch(err => {\n if (!this.watchdogState[err.code || '0'])\n this.watchdogState[err.code || '0'] = 0;\n if (Date.now() - this.watchdogState[err.code || '0'] > ((dcpConfig.worker.watchdogLogInterval * timeDilation || 120) * 1000))\n console.error('301: Failed to start work:', err);\n this.watchdogState[err.code || '0'] = Date.now();\n });\n }\n\n this.pruneSandboxes();\n }\n }\n\n /**\n * Gets the logical and physical number of cores and also\n * the total number of sandboxes the worker is allowed to run\n *\n */\n getStatisticsCPU() {\n if (DCP_ENV.isBrowserPlatform) {\n return {\n worker: this.workerOpaqueId,\n lCores: window.navigator.hardwareConcurrency,\n pCores: dcpConfig.worker.pCores || window.navigator.hardwareConcurrency,\n sandbox: this.maxWorkingSandboxes\n }\n }\n\n return {\n worker: this.workerOpaqueId,\n lCores: requireNative('os').cpus().length,\n pCores: requireNative('physical-cpu-count'),\n sandbox: this.maxWorkingSandboxes\n }\n }\n\n /**\n * Call to start doing work on the network.\n * This is the one place where requests to fetch new slices are made.\n * After the initial slice and job are fetched it calls @this.distributeQueuedSlices.\n *\n * @returns {Promise<void>}\n */\n async work()\n {\n\n await this.setDefaultIdentityKeystore();\n\n // if connections don't exist, instantiate them. Needed every time worker is started/stopped\n if (!this.taskDistributorConnection)\n this.instantiateAllConnections();\n\n let slicesToFetch;\n if (this.options.priorityOnly && this.options.jobAddresses.length === 0) {\n slicesToFetch = 0;\n } else if (this.queuedSlices.length > 1) {\n // We have slices queued, no need to fetch\n slicesToFetch = 0;\n } else {\n // The queue is almost empty (there may be 0 or 1 element), fetch a full task.\n // The task is full, in the sense that it will contain slices whose\n // aggregate execution time is this.maxWorkingSandboxes * 5-minutes.\n // However, there can only be this.unallocatedSpace # of long slices.\n // Thus we need to know whether a slice in this.queuedSlices is long or not.\n // (A long slice has estimated execution time > 5-minutes.)\n const longSliceCount = (this.queuedSlices.length > 0 && this.queuedSlices[0].isLongSlice) ? 1 : 0;\n slicesToFetch = this.unallocatedSpace - longSliceCount;\n }\n\n debugging() && console.log(`Supervisor.work: Try to get ${slicesToFetch} slices in working sandboxes, unallocatedSpace ${this.unallocatedSpace}, queued slices ${this.queuedSlices.length}; isFetchingNewWork: ${this.isFetchingNewWork}`);\n \n // Fetch a new task if we have no more slices queued, then start workers\n try {\n if (slicesToFetch > 0 && !this.isFetchingNewWork) {\n this.isFetchingNewWork = true;\n\n /**\n * This will only ready sandboxes up to a total count of\n * maxWorkingSandboxes (in any state). It is not possible to know the\n * actual number of sandboxes required until we have the slices because we\n * may have sandboxes assigned for the slice's job already.\n *\n * If the evaluator cannot start (ie. if the evalServer is not running),\n * then the while loop will keep retrying until the evalServer comes online\n */\n while (this.maxWorkingSandboxes > this.sandboxes.length) {\n await this.readySandboxes(1)\n .then(() => this.readySandboxes(this.maxWorkingSandboxes - this.sandboxes.length))\n .catch (error => {\n console.warn(`906: failed to ready sandboxes; will retry`, error.code, error.message);\n });\n }\n\n /**\n * Temporary change: Assign the capabilities of one of readied sandboxes\n * before fetching slices from the scheduler.\n *\n * TODO: Remove this once fetchTask uses the capabilities of every\n * sandbox to fetch slices.\n */\n if (!this.capabilities) {\n this.capabilities = this.sandboxes[0].capabilities;\n this.emit('capabilitiesCalculated', this.capabilities);\n }\n\n if(DCP_ENV.isBrowserPlatform && this.capabilities.browser)\n this.capabilities.browser.chrome = DCP_ENV.isBrowserChrome;\n\n const fetchTimeout = setTimeout(() => {\n console.warn(`679: Fetch exceeded timeout, will reconnect at next watchdog interval`);\n \n this.taskDistributorConnection.close('Fetch timed out', true).catch(error => {\n console.error(`931: Failed to close task-distributor connection`, error);\n });\n this.resultSubmitterConnection.close('Fetch timed out', true).catch(error => {\n console.error(`920: Failed to close result-submitter connection`, error);\n });\n this.isFetchingNewWork = false;\n }, 3 * 60 * 1000); // max out at 3 minutes to fetch\n\n // ensure result submitter connection before fetching tasks\n try\n {\n await this.resultSubmitterConnection.keepalive();\n }\n catch (error)\n {\n console.error('Failed to connect to result submitter, refusing to fetch slices. Will try again at next fetch cycle.')\n debugging('supervisor') && console.log(`Error: ${error}`);\n this.isFetchingNewWork = false; // <-- done in the `finally` block, below\n clearTimeout(fetchTimeout);\n this.taskDistributorConnection.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;\n }\n await this.fetchTask(slicesToFetch).finally(() => {\n clearTimeout(fetchTimeout);\n this.isFetchingNewWork = false;\n });\n }\n \n this.distributeQueuedSlices().then(() => debugging('supervisor') && 'supervisor: finished distributeQueuedSlices()').catch((e)=>console.error(e)) ;\n// await this.distributeQueuedSlices();\n // No catch(), because it will bubble outward to the caller\n } finally {\n \n }\n }\n\n /**\n * Generate the workerComputeGroups property of the requestTask message. \n * \n * Concatenate the compute groups object from dcpConfig with the list of compute groups\n * from the supervisor, and remove the public group if accidentally present. Finally,\n * we transform joinSecrets/joinHash into joinHashHash for secure transmission.\n *\n * @note computeGroup objects with joinSecrets are mutated to record their hashes. This\n * affects the supervisor options and dcpConfig. Re-adding a joinSecret property\n * to one of these will cause the hash to be recomputed.\n */\n generateWorkerComputeGroups()\n {\n var computeGroups = Object.values(dcpConfig.worker.computeGroups || {});\n if (this.options.computeGroups)\n computeGroups = computeGroups.concat(this.options.computeGroups);\n computeGroups = computeGroups.filter(group => group.id !== constants.computeGroups.public.id);\n const hashedComputeGroups = [];\n for (const group of computeGroups)\n {\n const groupCopy = Object.assign({}, group);\n if ((group.joinSecret || group.joinHash) && (!group.joinHashHash || this.lastDcpsid !== this.taskDistributorConnection.dcpsid))\n {\n let joinHash;\n if(group.joinHash) {\n joinHash = group.joinHash.replace(/\\s+/g, ''); // strip whitespace\n } else {\n joinHash = calculateJoinHash(groupCopy);\n } \n\n groupCopy.joinHashHash = hash.calculate(hash.eh1, joinHash, this.taskDistributorConnection.dcpsid);\n delete groupCopy.joinSecret;\n delete groupCopy.joinHash;\n debugging('computeGroups') && console.debug(`Calculated joinHash=${joinHash} for`, groupCopy);\n }\n hashedComputeGroups.push(groupCopy);\n }\n this.lastDcpsid = this.taskDistributorConnection.dcpsid;\n debugging('computeGroups') && console.debug('Requesting ', computeGroups.length, 'non-public groups for session', this.lastDcpsid);\n return hashedComputeGroups;\n }\n\n /**\n * Fetches a task, which contains job information and slices for sandboxes and\n * manages events related to fetching tasks so the UI can more clearly display\n * to user what is actually happening.\n * @param {number} numCores\n * @returns {Promise<void>} The requestTask request, resolve on success, rejects otherwise.\n * @emits Supervisor#fetchingTask\n * @emits Supervisor#fetchedTask\n */\n async fetchTask(numCores)\n {\n this.emit('fetchingTask');\n debugging('supervisor') && console.debug('supervisor: fetching task');\n const requestPayload = {\n numCores,\n coreStats: this.getStatisticsCPU(),\n numGPUs: this.defaultMaxGPUs,\n capabilities: this.capabilities,\n paymentAddress: this.paymentAddress,\n jobAddresses: this.options.jobAddresses, // when set, only fetches slices for these jobs\n localExec: this.options.localExec,\n workerComputeGroups: this.generateWorkerComputeGroups(),\n minimumWage: dcpConfig.worker.minimumWage || this.options.minimumWage,\n readyJobs: [ /* list of jobs addresses XXXwg */ ],\n previouslyWorkedJobs: this.cache.jobs,\n };\n\n // workers should be part of the public compute group by default\n if (!booley(dcpConfig.worker.leavePublicGroup) && !booley(this.options.leavePublicGroup))\n requestPayload.workerComputeGroups.push(constants.computeGroups.public);\n debugging('computeGroups') && console.log(`Fetching work for ${requestPayload.workerComputeGroups.length} ComputeGroups: `, requestPayload.workerComputeGroups);\n \n debugging() && console.log(`${shortTime()} fetchTask wants ${numCores} slices, unallocatedSpace ${this.unallocatedSpace}, queuedSlices ${this.queuedSlices.length}`);\n try {\n debugging('requestTask') && debug('requestPayload', requestPayload);\n\n let result = await this.taskDistributorConnection.send('requestTask', requestPayload);\n let responsePayload = result.payload; \n \n if (!result.success) { \n debugging() && console.log('Task fetch failure; request=', requestPayload);\n debugging() && console.log('Task fetch failure; response=', result.payload);\n throw new DCPError('Unable to fetch task for worker', responsePayload);\n }\n\n const sliceCount = responsePayload.body.task.length || 0;\n\n /**\n * The fetchedTask event fires when the supervisor has finished trying to\n * fetch work from the scheduler (task-manager). The data emitted is the\n * number of new slices to work on in the fetched task.\n *\n * @event Supervisor#fetchedTask\n * @type {number}\n */\n this.emit('fetchedTask', sliceCount);\n\n if (sliceCount < 1) {\n return;\n }\n\n /*\n * DCP-1698 Send auth msg with tasks to worker, then validate authority of worker to send slice info back to scheduler.\n * payload structure: { owner: this.address, signature: signature, auth: messageLightWeight, body: messageBody };\n * XXXpfr: @todo Do not think we need 'owner', think about it.\n */\n const { body, ...authorizationMessage } = responsePayload;\n const { newJobs, task } = body;\n assert(newJobs); // It should not be possible to have !newJobs -- we throw on !success.\n for (const jobAddress of Object.keys(newJobs))\n if(!this.cache.cache.job[jobAddress])\n this.cache.store('job', jobAddress, newJobs[jobAddress]);\n\n debugging('supervisor') && console.log(`${shortTime()} fetchTask: Task has ${task.length} slices from ${Object.keys(newJobs).length} new jobs.`);\n\n const tmpQueuedSlices = [];\n for (const taskElement of task) {\n // Memoize authMessage onto the Slice object, this should\n // follow it for its entire life in the worker\n const slice = new Slice(taskElement);\n slice.authorizationMessage = authorizationMessage;\n\n tmpQueuedSlices.push(slice);\n debugging('supervisor') && console.log(`/t${shortTime()} fetchTask: slice ${taskElement.sliceNumber}, jobAddress ${taskElement.jobAddress}`);\n }\n\n /** XXXpfr @todo Try to get rid of this.slices. */\n this.slices.push(...tmpQueuedSlices);\n\n this.queuedSlices = [...tmpQueuedSlices, ...this.queuedSlices]; /* Make sure the old ones are up front. */\n\n } catch (error) {\n this.emit('fetchTaskFailed', error);\n debugging('supervisor') && console.debug(`Supervisor.fetchTask failed!: error: ${error}`);\n }\n }\n\n /**\n * For each slice in this.queuedSlices, match with a sandbox in the following order:\n * 1. Try to find an already assigned sandbox in this.assignedSandboxes for the slice's job.\n * 2. Find a ready sandbox in this.readiedSandboxes that is unassigned.\n * 3. Ready a new sandbox and use that.\n *\n * Take great care in assuring sandboxes and slices are uniquely associated, viz.,\n * a given slice cannot be associated with multiple sandboxes and a given sandbox cannot be associated with multiple slices.\n * The lack of such uniqueness has been the root cause of several difficult bugs.\n *\n * @param {number} numSlices The number of slices to match with sandboxes.\n * @returns {Promise<SandboxSlice[]>} Returns SandboxSlice[], may have length zero.\n */\n async matchSlicesWithSandboxes (numSlices) {\n const sandboxSlices = [];\n if (numSlices <= 0 || this.queuedSlices.length == 0 || this.matching)\n return sandboxSlices;\n\n // Don't ask for more than we have.\n if (numSlices > this.queuedSlices.length)\n numSlices = this.queuedSlices.length;\n\n debugging() && console.log(`${shortTime()} matchSlicesWithSandboxes: numSlices ${numSlices}, queued slices ${this.queuedSlices.length}: assigned ${this.assignedSandboxes.length}, readied ${this.readiedSandboxes.length}, total sandbox count: ${this.sandboxes.length}`);\n\n // Auxiliary function to mark sandboxes as allocated so they cannot be pruned out from under us.\n function markAsAllocated(sandboxArray, numMarked) {\n if (numMarked > sandboxArray.length) numMarked = sandboxArray.length;\n for (let k = 1; k <= numMarked; k++) {\n assert(!sandboxArray[sandboxArray.length - k].allocated);\n sandboxArray[sandboxArray.length - k].allocated = true;\n }\n }\n\n // Filter and sandboxes that are not ready out of the `readiedSandboxes` list. Ie filter\n // out sandboxes that have been terminated before being used.\n this.readiedSandboxes = this.readiedSandboxes.filter((sandbox) => sandbox.state === 'READY_FOR_ASSIGN')\n\n try\n {\n this.matching = true;\n const jobAssignedMap = {};\n\n // Until this is rock-solid-stable I want to check for uniquenees.\n if (true) {\n dumpSlicesIfNotUnique(this.queuedSlices, `DANGER: this.queuedSlices slices are not unique -- this is ok when slice is rescheduled.`);\n dumpSandboxesIfNotUnique(this.readiedSandboxes, `DANGER: this.readiedSandboxes sandboxes are not unique!`);\n dumpSandboxesIfNotUnique(this.assignedSandboxes, `DANGER: this.assignedSandboxes sandboxes are not unique!`);\n }\n\n if (debugging()) {\n dumpSlices(this.queuedSlices, 'matchSlicesWithSandboxes(top): this.queuedSlices');\n dumpSandboxes(this.assignedSandboxes, 'matchSlicesWithSandboxes(top): this.assignedSandboxes');\n dumpSandboxes(this.readiedSandboxes, 'matchSlicesWithSandboxes(top): this.readiedSandboxes');\n }\n\n let counter = 0; // General counter.\n let assignedCounter = 0; // How many assigned sandboxes are being used.\n let readyCounter = 0; // How many sandboxes used from the existing this.queuedSlices.\n let newCounter = 0; // How many sandboxes that needed to be newly created.\n\n //\n // The Ideas:\n // 1) We match each slice with a sandbox, in the order that they appear in this.queuedSlices.\n // This allows us to try different orderings of execution of slices. E.g. Wes suggested\n // trying to execute slices from different jobs with maximal job diversity -- specifically\n // if there are 3 jobs j1,j2,j3, with slices s11, s12 from j1, s21, s22, s23 from j2 and\n // s31, s32 from j3, then we try to schedule, in order s11, s21, s31, s12, s22, s32, s23.\n //\n // 2) Before matching slices with sandboxes, we allocate available assigned and readied sandboxes\n // and if more are needed then we create and allocate new ones.\n //\n // 3) Finally we match slices with sandboxes and return an array of sandboxSlice pairs.\n //\n \n //\n // General description of the complexity of promise races.\n // NOTE: Supervisor.returnSandbox does not call any promises and hence does not need to be async, so that has been dropped.\n // The added benefit is that Supervisor.pruneSandboxes is also not async anymore and hence there is better safety with the\n // race between Supervisor.pruneSandboxes and Supervisor.readySandboxes. Specifically, during pruneSandboxes there are no\n // promise calls which could result in 'async readySandboxes' (which creates new 'readied' sandboxes) being called in the midst\n // of pruning 'readied' sandboxes. There is still a race in 'async matchSlicesWithSandboxes' when calling 'async readySandboxes'\n // where it is possible to interleave with 'async watchdog' which calls pruneSandboxes, but this race is prevented by setting\n // 'this.doNotPrune = true;' in 'async matchSlicesWithSandboxes' right before the call to 'async readySandboxes', because\n // 'if (!this.doNotPrune)' guards pruning 'readied' sandboxes.\n //\n\n // Associate assigned sandboxes for a given jobAddress with the corresponding count of slices owned\n // by the same jobAddress.\n for (let index = this.queuedSlices.length - 1; index >= 0; index--) {\n const slice = this.queuedSlices[index];\n assert(slice.isUnassigned);\n\n const jobAddress = slice.jobAddress.valueOf();\n let assignedState = jobAssignedMap[jobAddress];\n\n if (!assignedState) {\n // Get all assigned sandboxes for jobAddress.\n assignedState = { sandboxes: this.assignedSandboxes.filter(sb => sb.jobAddress.valueOf() === jobAddress), count: 1 };\n jobAssignedMap[jobAddress] = assignedState;\n } else {\n assignedState.count++;\n }\n\n if (++counter >= numSlices)\n break;\n }\n counter = 0;\n\n // Calculate how many new sandboxes are needed.\n for (const [jobAddress, assignedState] of Object.entries(jobAssignedMap)) {\n // Count the # of assigned sandboxes we can use.\n const assignedUsedCount = Math.min(assignedState.count, assignedState.sandboxes.length);\n assignedCounter += assignedUsedCount;\n\n // Mark the assigned sandboxes we can use as allocated, so they're not pruned out from under us.\n markAsAllocated(assignedState.sandboxes, assignedUsedCount);\n\n // Count slices w/o assigned sandbox.\n const count = Math.max(assignedState.count - assignedState.sandboxes.length, 0);\n counter += count;\n debugging() && console.log(`matchSlicesWithSandboxes: job ${jobAddress}, assigned ${assignedState.sandboxes.length}, slices ${assignedState.count}, need ${count}/${this.readiedSandboxes.length} readied sandboxes.`);\n }\n\n counter -= this.readiedSandboxes.length;\n if (counter > 0) { // Number of new sandboxes needed.\n newCounter = counter;\n readyCounter = this.readiedSandboxes.length;\n // Don't prune readied sandboxes while creating readied sandboxes -- yes, it happens...\n this.doNotPrune = true;\n try { await this.readySandboxes(newCounter); } finally { this.doNotPrune = false; }\n } else {\n readyCounter = this.readiedSandboxes.length + counter;\n }\n counter = 0;\n\n debugging() && console.log(`matchSlicesWithSandboxes: newCounter ${newCounter}, readyCounter ${readyCounter}, assignedCounter ${assignedCounter}, total readied ${this.readiedSandboxes.length}`);\n\n // Validate algorithm consistency.\n if (Supervisor.debugBuild && assignedCounter + readyCounter + newCounter !== numSlices) {\n // Structured assert.\n throw new DCPError(`matchSlicesWithSandboxes: Algorithm is corrupt ${assignedCounter} + ${readyCounter} + ${newCounter} !== ${numSlices}`);\n }\n\n // Mark the readied sandboxes we can use as allocated, so they're not pruned out from under us.\n // Note: We could run markAsAllocated before the async call to readySandboxes, which would gaurd from pruning,\n // then run it again on the brand new sandboxes just created, but it makes the code hard to read and isn't necessary,\n // beacause the doNotPrune trick seems to work. We also check if we unexpectedly run out of sandboxes (assert in debug.)\n markAsAllocated(this.readiedSandboxes, readyCounter + newCounter);\n\n while (this.queuedSlices.length > 0) {\n const slice = this.queuedSlices.pop();\n assert(slice.isUnassigned);\n\n const jobAddress = slice.jobAddress.valueOf();\n const assignedSandboxesForJob = jobAssignedMap[jobAddress].sandboxes;\n assert(assignedSandboxesForJob);\n\n if (assignedSandboxesForJob.length > 0) {\n const sandbox = assignedSandboxesForJob.pop();\n this.removeElement(this.assignedSandboxes, sandbox);\n assert(jobAddress === sandbox.jobAddress.valueOf());\n\n debugging() && console.log(`matchSlicesWithSandboxes: Assigned sandbox matched ${this.dumpSandboxAndSlice(sandbox, slice)}`);\n sandboxSlices.push({ sandbox, slice });\n } else {\n // There may be a race between creating and pruning, though it should be fixed with the doNotPrune trick.\n assert(this.readiedSandboxes.length > 0);\n if (this.readiedSandboxes.length > 0) {\n const sandbox = this.readiedSandboxes.pop();\n debugging() && console.log(`matchSlicesWithSandboxes: Readied sandbox matched ${this.dumpSandboxAndSlice(sandbox, slice)}`);\n sandboxSlices.push({ sandbox, slice });\n }\n }\n\n if (++counter >= numSlices)\n break;\n }\n\n debugging() && console.log(`matchSlicesWithSandboxes: Matches: ${ JSON.stringify(sandboxSlices.map(ss => this.dumpSandboxSlice(ss))) }`);\n\n // Until this is rock-solid-stable I want to check for uniquenees.\n if (true) {\n this.dumpSandboxSlicesIfNotUnique(sandboxSlices, `DANGER: sandboxSlices; { sandbox, slice } pairs are not unique!`);\n }\n\n if (debugging()) {\n console.log(`matchSlicesWithSandboxes: found ${sandboxSlices.length} sandboxes for jobs ${JSON.stringify(Object.keys(jobAssignedMap))}: assigned ${assignedCounter}, ready ${readyCounter}, new ${newCounter}`);\n this.dumpSandboxSlices(sandboxSlices, 'matchSlicesWithSandboxes: sandboxSlices');\n dumpSandboxes(this.assignedSandboxes, 'matchSlicesWithSandboxes: this.assignedSandboxes');\n dumpSandboxes(this.readiedSandboxes, 'matchSlicesWithSandboxes: this.readiedSandboxes');\n console.log(`matchSlicesWithSandboxes: remaining readied: ${JSON.stringify(this.readiedSandboxes.map(s => s.id))}`);\n }\n\n } catch (e) {\n console.error(`DANGER matchSlicesWithSandboxes threw exception`, e);\n //throw e; // Should we rethrow?\n } finally {\n this.matching = false;\n }\n\n debugging() && console.log(`${shortTime()} matchSlicesWithSandboxes allocated ${sandboxSlices.length} sandboxes, queuedSlices ${this.queuedSlices.length}, unallocatedSpace ${this.unallocatedSpace}.`);\n\n return sandboxSlices;\n }\n\n /**\n * This method will call @this.startSandboxWork(sandbox, slice) for { sandbox, slice }\n * in the array returned by @this.matchSlicesWithSandboxes(availableSandboxes) until all sandboxes are working.\n * It is possible for a sandbox to finish simultaneously and leave a sandbox that is not working.\n * Note: @this.queuedSlices may be exhausted before all sandboxes are working.\n * @returns {Promise<void>}\n */\n async distributeQueuedSlices () {\n //\n // All matching of sandbox with slice is taken care of by @this.matchSlicesWithSandboxes().\n // We do not use @this.snapshotOfQueuedSlices becuase it is in a fuzzy state; when matched with a sandbox,\n // a slice transitions to working at an unknown point in the future.\n // Instead we use @this.queuedSlices which really is a queue, whose elements are\n // dequeued in @this.matchSlicesWithSandboxes() and enqueued in fetchTask.\n // We should try to get rid of @this.slices, because it has low utility.\n // However, to minimize churn, this can be done later.\n //\n\n const availableSandboxes = this.unallocatedSpace;\n if (availableSandboxes <= 0) return;\n\n debugging() && console.log(`distributeQueuedSlices: unallocatedSpace ${availableSandboxes}, readied ${JSON.stringify(this.readiedSandboxes.map(s => s.id))}, queuedSlices ${this.queuedSlices.length}`);\n\n const sandboxSlices = await this.matchSlicesWithSandboxes(availableSandboxes);\n\n for (let sandboxSlice of sandboxSlices) {\n\n const { sandbox, slice } = sandboxSlice;\n if (sandbox) {\n debugging() && console.log(`${shortTime()}: distributeQueuedSlices: matched slice ${slice.sliceNumber} with sandbox ${sandbox.id} for job ${slice.jobAddress}, total sandbox count: ${this.sandboxes.length}`);\n if (sandbox.isReadyForAssign) {\n try {\n let timeoutMs = Math.floor(Math.min(+Supervisor.lastAssignFailTimerMs || 0, 10 * 60 * 1000 /* 10m */));\n await asleepMs(timeoutMs);\n if (sandbox.isReadyForAssign) { // Don't need this double-check anymore because of @this.matchSlicesWithSandboxes().\n sandbox.setIsAssigning(); // Don't need this either --> Set the state to assigning before the await to circumvent the event loop problem from await.\n await this.assignJobToSandbox(sandbox, slice.jobAddress);\n } else {\n return // Don't need this either --> The sandbox was picked a second time while it was being prepared, return without an error\n }\n } catch (e) {\n console.error(`Could not assign slice ${slice.sliceNumber} for job ${slice.jobAddress} to sandbox ${sandbox.id}`);\n if (Supervisor.debugBuild) console.error(`...exception`, e);\n Supervisor.lastAssignFailTimerMs = Supervisor.lastAssignFailTimerMs ? +Supervisor.lastAssignFailTimerMs * 1.25 : Math.random() * 200;\n this.returnSlice(slice);\n continue;\n }\n }\n\n if (!Supervisor.lastAssignFailTimerMs)\n Supervisor.lastAssignFailTimerMs = Math.random() * 200;\n this.startSandboxWork(sandbox, slice);\n Supervisor.lastAssignFailTimerMs = false;\n\n } else {\n // We should never get here.\n console.error(\"Supervisor.distributeQueuedSlices: Failed to find sandbox for slice\", {\n jobAddress: slice.jobAddress,\n sliceNumber: slice.sliceNumber\n });\n }\n }\n }\n\n /**\n *\n * @param {Sandbox} sandbox\n * @param {opaqueId} jobAddress\n * @returns {Promise<void>}\n */\n async assignJobToSandbox(sandbox, jobAddress) {\n var ceci = this;\n\n try {\n return sandbox.assign(jobAddress); // Returns Promise.\n } catch(error) {\n // return slice to scheduler, log error\n console.error('Supervisor.assignJobToSandbox: Failed to assign job to sandbox.', {\n jobAddress: jobAddress.substr(0,10),\n error,\n });\n\n ceci.returnSandbox(sandbox);\n\n throw error;\n }\n }\n \n /**\n * Gives a slice to a sandbox which begins working. Handles collecting\n * the slice result (complete/fail) from the sandbox and submitting the result to the scheduler.\n * It will also return the sandbox to @this.returnSandbox when completed so the sandbox can be re-assigned.\n *\n * @param {Sandbox} sandbox - the sandbox to give the slice\n * @param {Slice} slice - the slice to distribute\n * @returns {Promise<void>} Promise returned from sandbox.run\n */\n async startSandboxWork (sandbox, slice) {\n var startDelayMs;\n\n try {\n slice.markAsWorking();\n } catch (e) {\n // This will occur when the same slice is distributed twice.\n // It is normal because two sandboxes could finish at the same time and be assigned the\n // same slice before the slice is marked as working.\n debugging() && console.debug(e);\n return;\n }\n\n // sandbox.requiresGPU = slice.requiresGPU;\n // if (sandbox.requiresGPU) {\n // this.GPUsAssigned++;\n // }\n\n if (Supervisor.startSandboxWork_beenCalled)\n startDelayMs = 1000 * (tuning.minSandboxStartDelay + (Math.random() * (tuning.maxSandboxStartDelay - tuning.minSandboxStartDelay)));\n else {\n startDelayMs = 1000 * tuning.minSandboxStartDelay;\n Supervisor.startSandboxWork_beenCalled = true;\n }\n \n try {\n debugging() && console.log(`${shortTime()} startSandboxWork: started sandbox.id.sliceNumber ${sandbox.id}-${slice.sliceNumber}.${slice.jobAddress}, total sandbox count: ${this.sandboxes.length}`);\n let result = await sandbox.work(slice, startDelayMs);\n slice.collectResult(result, true);\n // In watchdog, all sandboxes in working state, have their status sent to result submitter.\n // However, this can happen after the sandbox/slice has already sent results\n // to result submitter, in which case, the activeSlices table has already removed the row\n // corresponding to slice and hence is incapable of updating status.\n sandbox.changeWorkingToAssigned();\n sandbox.allocated = false; // This is a little redundant: don't await any promises between here and the finally clause.\n this.assignedSandboxes.push(sandbox);\n debugging() && console.log(`${shortTime()} startSandboxWork: completed sandbox.id.sliceNumber ${sandbox.id}-${slice.sliceNumber}.${slice.jobAddress}, total sandbox count: ${this.sandboxes.length}`);\n } catch(error) {\n let logLevel;\n if (error instanceof SandboxError) {\n logLevel = 'warn';\n // The message and stack properties of error objects are not enumerable,\n // so they have to be copied into a plain object this way\n const errorResult = Object.getOwnPropertyNames(error).reduce((o, p) => {\n o[p] = error[p]; return o;\n }, { message: 'Unexpected worker error' });\n slice.collectResult(errorResult, false);\n } else {\n logLevel = 'error';\n // This error was unrelated to the work being done,\n // so just return the slice in the finally block.\n }\n\n // sandbox.public.name is defined in Sandbox.assign.\n // It is possible to get here with !sandbox.public.\n const jobName = sandbox.public ? sandbox.public.name : 'unnamed';\n const errorObject = {\n jobAddress: slice.jobAddress.substr(0,10),\n sliceNumber: slice.sliceNumber,\n sandbox: sandbox.id,\n jobName: jobName,\n };\n\n // XXXpfr Enabled Informative sandbox errors when in debug mode.\n if (!Supervisor.debugBuild && error.errorCode === 'EUNCAUGHT') {\n console[logLevel](`Supervisor.startSandboxWork - Uncaught error in sandbox, could not compute.\\n`, errorObject);\n } else if (!Supervisor.debugBuild && error.errorCode === 'EFETCH') {\n console[logLevel](`Supervisor.startSandboxWork - Could not fetch data: ${error.message}`);\n } else {\n if (Supervisor.debugBuild) // Don't show stack traces in release builds.\n errorObject.stack = error.stack;\n console[logLevel](`Supervisor.startSandboxWork::Sandbox failed!\\n`, errorObject);\n }\n } finally {\n sandbox.allocated = false;\n\n if (slice.result) {\n await this.recordResult(slice);\n } else {\n this.returnSlice(slice);\n }\n this.returnSandbox(sandbox, slice);\n }\n }\n\n /**\n * Terminates sandboxes and returns slices.\n * Sets the working flag to false, call @this.work to start working again.\n * \n * If forceTerminate is true: Terminates all sandboxes and returns all slices.\n * If forceTerminate is false: Terminates non-working sandboxes and returns queued slices.\n *\n * @param {boolean} [forceTerminate = true] - true if you want to stop the sandboxes from completing their current slice.\n * @returns {Promise<void>}\n */\n async stopWork (forceTerminate = true) {\n if (forceTerminate) {\n let sandbox;\n while (sandbox = this.sandboxes.pop()) {\n sandbox.terminate(false);\n }\n\n await this.returnSlices(this.slices).then(() => {\n this.slices.length = 0;\n this.queuedSlices.length = 0;\n });\n\n this.resultSubmitterConnection.off('close', this.openResultSubmitterConn);\n this.resultSubmitterConnection.close();\n this.resultSubmitterConnection = null;\n } else {\n // Only terminate idle sandboxes and return only queued slices\n let idleSandboxes = this.sandboxes.filter(w => !w.isWorking);\n for (let sandbox of idleSandboxes) {\n sandbox.terminate(false);\n }\n \n await this.returnSlices(this.slices).then(() => {\n this.slices.length = 0;\n this.queuedSlices.length = 0;\n });\n\n await new Promise((resolve, reject) => {\n let sandboxesRemaining = this.workingSandboxes.length;\n if (sandboxesRemaining === 0)\n {\n resolve();\n }\n // Resolve and finish work once all sandboxes have finished submitting their results.\n this.on('submitFinished', () => {\n sandboxesRemaining--;\n if (sandboxesRemaining === 0)\n {\n console.log('All sandboxes empty, stopping worker and closing all connections');\n resolve();\n }\n });\n })\n this.resultSubmitterConnection.off('close', this.openResultSubmitterConn);\n this.resultSubmitterConnection.close();\n this.resultSubmitterConnection = null;\n }\n\n this.taskDistributorConnection.off('close', this.openTaskDistributorConn);\n this.taskDistributorConnection.close();\n this.taskDistributorConnection = null;\n\n this.packageManagerConnection.off('close', this.openPackageManagerConn);\n this.packageManagerConnection.close();\n this.packageManagerConnection = null;\n\n this.eventRouterConnection.off('close', this.openEventRouterConn);\n this.eventRouterConnection.close();\n this.eventRouterConnection = null;\n\n this.emit('stop');\n }\n\n /**\n * Takes a slice and returns it to the scheduler to be redistributed.\n * Usually called when the supervisor told it to forcibly stop working.\n *\n * @param {Slice} slice - the slice to return to the scheduler\n * @returns {Promise<*>} - response from the scheduler the slice was returned to\n */\n returnSlice (slice) {\n\n const jobAddress = slice.jobAddress;\n const sliceNumber = slice.sliceNumber;\n \n const authorizationMessage = this.findAuthorizationMessage('Supervisor.returnSlice:', jobAddress, sliceNumber);\n \n // Remove the slice from the slices array.\n this.removeElement(this.slices, slice);\n\n let payload = {};\n if (slice.error) {\n payload = slice.getReturnMessagePayload(this.workerOpaqueId, authorizationMessage, slice.error.errorCode);\n } else {\n payload = slice.getReturnMessagePayload(this.workerOpaqueId, authorizationMessage);\n }\n\n return this.resultSubmitterConnection.send('status', payload)\n .then(response => {\n return response;\n })\n .catch(error => {\n console.error('Failed to return slice', {\n sliceIdentifier: slice.identifier,\n sliceNumber: sliceNumber,\n jobAddress: jobAddress,\n error,\n });\n })\n }\n\n /** Bulk-return multiple slices, possibly for assorted jobs\n */\n returnSlices(slices) {\n const slicePayload = [];\n\n slices.forEach(slice => {\n addToSlicePayload(slicePayload, slice.jobAddress, slice.sliceNumber, 'return', this.findAuthorizationMessage('Supervisor.returnSlices:', slice.jobAddress, slice.sliceNumber));\n });\n\n return this.resultSubmitterConnection.send('status', {\n worker: this.workerOpaqueId,\n slices: slicePayload,\n });\n }\n\n /**\n * Submits the slice results to the scheduler, either to the\n * work submit or fail endpoints based on the slice status.\n * Then remove the slice from the @this.slices cache.\n *\n * @param {Slice} slice - the slice to submit\n * @returns {Promise<void>}\n */\n async recordResult (slice) {\n debugging('supervisor') && console.log('supervisor: recording result');\n\n // It is possible for slice.result to be undefined when there are upstream errors.\n if ( !(slice && slice.result))\n throw new Error(`recordResult: slice.result is undefined for sliceNumber ${slice.sliceNumber}, jobAdress ${slice.jobAddress}. This is ok when there are upstream errors.`);\n\n const jobAddress = slice.jobAddress;\n const sliceNumber = slice.sliceNumber;\n const authorizationMessage = this.findAuthorizationMessage('Supervisor.recordResult:', jobAddress, sliceNumber);\n\n /* @see result-submitter::result for full message details */\n const metrics = { GPUTime: 0, CPUTime: 0, CPUDensity: 0, GPUDensity: 0 };\n var payloadData = {\n slice: sliceNumber,\n job: jobAddress,\n worker: this.workerOpaqueId,\n paymentAddress: this.paymentAddress,\n metrics,\n authorizationMessage,\n }\n const timeReport = slice.result.timeReport;\n if (timeReport && timeReport.total > 0) {\n metrics.GPUTime = timeReport.webGL;\n metrics.CPUTime = timeReport.CPU;\n metrics.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() && console.log('Supervisor.recordResult: payloadData.result', payloadData.result.slice(0, 512));\n\n try {\n if (slice.completed) {\n\n /* work function returned a result */\n const { success, payload } = await this.resultSubmitterConnection.send(\n 'result',\n payloadData,\n );\n\n if (!success) {\n throw new DCPError('Unable to submit result for work done', payload);\n }\n\n const receipt = {\n accepted: true,\n payment: payload.slicePaymentAmount,\n };\n this.emit('submittedResult', payload);\n this.emit('dccCredit', receipt);\n } else {\n /* slice did not complete for some reason */\n let statusPayloadData = {\n worker: this.workerOpaqueId,\n slices: [\n {\n sliceNumber: sliceNumber,\n job: jobAddress,\n status: 'return', // special state looked for in status.js / result-submitter\n reason: 'uncaught', // special state looked for in status.js / result-submitter\n error: slice.error,\n authorizationMessage,\n }\n ], \n };\n \n await this.resultSubmitterConnection.send('status', statusPayloadData);\n }\n } catch(error) {\n console.info(`1014: Failed to submit results for slice ${payloadData.slice} of job ${payloadData.job}`, error);\n this.emit('submitSliceFailed', error);\n }\n finally\n {\n this.emit('submitFinished');\n // Remove the slice from the slices array\n this.removeElement(this.slices, slice);\n }\n }\n\n /**\n * Send a work function's result to a server that speaks our DCP Remote Data Server protocol.\n * The data server dcp-rds is been implemented in https://gitlab.com/Distributed-Compute-Protocol/dcp-slice-service .\n *\n * @param {Slice} slice - Slice object whose result we are sending.\n * @returns {Promise<object>} - Object of the form { success: true, href: 'http://127.0.0.1:3521/methods/download/jobs/34/result/10' } .\n * @throws When HTTP status not in the 2xx range.\n */\n async sendResultToRemote(slice) {\n const postParams = {\n ...slice.resultStorageParams\n };\n\n const sliceResultUri = makeDataURI('pattern', slice.resultStorageDetails, {\n slice: slice.sliceNumber,\n job: slice.jobAddress,\n });\n\n debugging() && console.log('sendResultToRemote sliceResultUri: ', sliceResultUri);\n\n const url = new DcpURL(sliceResultUri);\n\n // Note: sendResultToRemote was made a member function of class Supervisor to enable access to this.alowedOrigins .\n if (this.allowedOrigins.indexOf(url.origin) === -1 &&\n dcpConfig.worker.allowOrigins.sendResults.indexOf(url.origin) === -1) {\n throw new Error(`Invalid origin for remote result storage: '${url.origin}'`);\n }\n\n postParams.element = slice.sliceNumber;\n postParams.contentType = 'application/json'; // Currently data will be outputed as a JSON object, @todo: Support file upload.\n\n debugging() && console.log('sendResultToRemote: postParams: ', postParams);\n\n let result = slice.result.result;\n if (result) {\n // The value being sent may be fetched with python requests so cannot be kvin serialized.\n if (result.hasOwnProperty('_serializeVerId')) {\n throw new Error('sendResultToRemote: slice.result.result must not be kvin serialized.')\n // result = require('kvin').unmarshal(result);\n }\n postParams.content = JSON.stringify(result);\n } else {\n postParams.error = JSON.stringify(slice.error);\n }\n\n debugging() && console.log('sendResultToRemote: content: ', (result ? postParams.content : postParams.error).slice(0, 512));\n\n //\n // Notes:\n // 1) In recordResults the response from justFetch is JSON serialized and encodeDataURI is called.\n // payloadData.result = await this.sendResultToRemote(slice);\n // payloadData.result = encodeDataURI(JSON.stringify(payloadData.result));\n // 2) We do further processing after the call to sendResultToRemote in recordResult, because\n // if we did it here there would be a perf hit. When the return value is a promise, it gets\n // folded into sendResultToRemote's main promise. If justFetch's promise wasn't a return value then\n // justFetch would be separately added to the micro-task-queue.\n return await justFetch(url, 'JSON', 'POST', false, postParams);\n }\n}\n\n/**\n * Return DCPv4-specific connection options, composed of type-specific, URL-specific, \n * and worker-specific options, any/all of which can override the dcpConfig.dcp.connectOptions.\n * The order of precedence is the order of specificity.\n */\nfunction connectionOptions(url, label) {\n return leafMerge(/* ordered from most to least specific */\n dcpConfig.worker.dcp.connectionOptions.default,\n dcpConfig.worker.dcp.connectionOptions[label],\n dcpConfig.worker.dcp.connectionOptions[url.href]);\n}\n\n\n/**\n * Add a slice to the slice payload being built. If a sliceList already exists for the \n * job-status-authMessage tuple, then the slice will be added to that, otherwise a new \n * sliceList will be added to the payload.\n *\n * @param {Object[]} slicePayload Slice payload being built. Will be mutated in place.\n * @param {Address} job Address of job the slice belongs to\n * @param {Number} sliceNumber Slice number\n * @param {String} status Status update, eg. progress or scheduled\n * @param {Object} authorizationMessage authorizationMessage for the slice\n *\n * @returns {Object[]} mutated slicePayload array\n */\nfunction addToSlicePayload(slicePayload, job, sliceNumber, status, authorizationMessage) {\n // Try to find a sliceList in the payload which matches the job, status, and auth message\n let sliceList = slicePayload.find(desc => {\n return desc.job === job && desc.status === status && desc.authorizationMessage === authorizationMessage;\n });\n\n // If we didn't find a sliceList, start a new one and add it to the payload\n if (!sliceList) {\n sliceList = {\n job,\n sliceNumbers: [],\n status,\n authorizationMessage: authorizationMessage,\n };\n slicePayload.push(sliceList);\n }\n\n sliceList.sliceNumbers.push(sliceNumber);\n\n return slicePayload;\n}\n\n/** @type {number | boolean} */\nSupervisor.lastAssignFailTimerMs = false;\n/** @type {boolean} */\nSupervisor.startSandboxWork_beenCalled = false;\n/** @type {boolean} */\nSupervisor.debugBuild = (__webpack_require__(/*! dcp/common/dcp-build */ \"./src/common/dcp-build.js\").build === 'debug');\n\nexports.Supervisor = Supervisor;\n\n\n//# sourceURL=webpack:///./src/dcp-client/worker/supervisor.js?");
|
|
5389
|
+
eval("/**\n * @file worker/supervisor.js\n *\n * The component that controls each of the sandboxes\n * and distributes work to them. Also communicates with the\n * scheduler to fetch said work.\n *\n * The supervisor readies sandboxes before/while fetching slices.\n * This means sometimes there are extra instantiated WebWorkers\n * that are idle (in this.readiedSandboxes). Readied sandboxes can\n * be used for any slice. After a readied sandbox is given a slice\n * it becomes assigned to slice's job and can only do work\n * for that job.\n *\n * After a sandbox completes its work, the sandbox becomes cached\n * and can be reused if another slice with a matching job is fetched.\n *\n * @author Matthew Palma, mpalma@kingsds.network\n * Ryan Rossiter, ryan@kingsds.network\n * @date May 2019\n */\n\n/* global dcpConfig */\n// @ts-check\n\n\nconst constants = __webpack_require__(/*! dcp/common/scheduler-constants */ \"./src/common/scheduler-constants.js\");\nconst hash = __webpack_require__(/*! dcp/common/hash */ \"./src/common/hash.js\");\nconst wallet = __webpack_require__(/*! dcp/dcp-client/wallet */ \"./src/dcp-client/wallet/index.js\");\nconst protocolV4 = __webpack_require__(/*! dcp/protocol-v4 */ \"./src/protocol-v4/index.js\");\nconst DCP_ENV = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\n\nconst debugging = __webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope('worker');\nconst { assert } = __webpack_require__(/*! dcp/common/dcp-assert */ \"./src/common/dcp-assert.js\");\nconst { EventEmitter } = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\");\nconst { Sandbox, SandboxError } = __webpack_require__(/*! ./sandbox */ \"./src/dcp-client/worker/sandbox.js\");\nconst { Slice } = __webpack_require__(/*! ./slice */ \"./src/dcp-client/worker/slice.js\");\nconst { SupervisorCache } = __webpack_require__(/*! ./supervisor-cache */ \"./src/dcp-client/worker/supervisor-cache.js\");\nconst { DcpURL } = __webpack_require__(/*! dcp/common/dcp-url */ \"./src/common/dcp-url.js\");\nconst { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\nconst { localStorage } = __webpack_require__(/*! dcp/common/dcp-localstorage */ \"./src/common/dcp-localstorage.js\");\nconst { booley, encodeDataURI, makeDataURI, leafMerge, asleepMs, shortTime, justFetch,\n dumpSandboxes, dumpSlices, dumpSandboxesIfNotUnique, dumpSlicesIfNotUnique, 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 debug = (...args) => {\n if (debugging('supervisor')) {\n console.debug('dcp-client:worker:supervisor', ...args);\n }\n};\n\nconst supervisorTuning = dcpConfig.future('supervisor.tuning');\nconst tuning = {\n watchdogInterval: 7, /**< seconds - time between fetches when ENOTASK(? /wg nov 2019) */\n minSandboxStartDelay: 0.1, /**< seconds - minimum time between WebWorker starts */\n maxSandboxStartDelay: 0.7, /**< seconds - maximum delay time between WebWorker starts */\n ...supervisorTuning\n};\n\n/** Make timers 10x slower when running in niim */\nlet timeDilation = 1;\nif (DCP_ENV.platform === 'nodejs') {\n /** Make timers 10x slower when running in niim */\n timeDilation = (requireNative('module')._cache.niim instanceof requireNative('module').Module) ? 10 : 1;\n}\n\ndcpConfig.future('worker.sandbox', { progressReportInterval: (5 * 60 * 1000) });\nconst sandboxTuning = dcpConfig.worker.sandbox;\n\n/**\n * @typedef {*} opaqueId\n */\n\n/**\n * @typedef {object} SandboxSlice\n * @property {Sandbox} sandbox\n * @property {Slice} slice\n */\n\n/**\n * @typedef {object} Signature\n * @property {Uint8Array} r\n * @property {Uint8Array} s\n * @property {Uint8Array} v\n */\n\n/**\n * @typedef {object} SignedAuthorizationMessageObject\n * @property {object} auth\n * @property {Signature} signature\n * @property {module:dcp/wallet.Address} owner\n */\n\n/** @typedef {import('.').Worker} Worker */\n/** @typedef {import('.').SupervisorOptions} SupervisorOptions */\n\nclass Supervisor extends EventEmitter {\n /**\n * @constructor\n * @param {Worker} worker\n * @param {SupervisorOptions} options\n */\n constructor (worker, options={}) {\n super('Supervisor');\n\n /** @type {Worker} */\n this.worker = worker;\n\n /** @type {Sandbox[]} */\n this.sandboxes = [];\n\n /**\n * XXXpfr @todo We may still needs this.slices to communicate state to result-submitter status endpoint, but it isn't used otherwise.\n * @deprecated\n * @type {Slice[]}\n */\n this.slices = [];\n\n /** @type {Slice[]} */\n this.queuedSlices = [];\n\n /** @type {Sandbox[]} */\n this.readiedSandboxes = [];\n\n /** @type {Sandbox[]} */\n this.assignedSandboxes = [];\n\n /** @type {boolean} */\n this.matching = false;\n\n /** @type {boolean} */\n this.doNotPrune = false;\n\n if (!options) {\n console.error('Supervisor Options', options, new Error().stack);\n options = {};\n }\n \n /** @type {object} */\n this.options = {\n jobAddresses: options.jobAddresses || [/* all jobs unless priorityOnly */],\n ...options,\n };\n\n const { paymentAddress, identityKeystore } = options;\n if (paymentAddress) {\n if (paymentAddress instanceof wallet.Keystore) {\n this.paymentAddress = paymentAddress.address;\n } else {\n this.paymentAddress = new wallet.Address(paymentAddress);\n }\n } else {\n this.paymentAddress = null;\n }\n\n this._identityKeystore = identityKeystore;\n\n this.allowedOrigins = dcpConfig.worker.allowOrigins.any;\n if(options.allowedOrigins && options.allowedOrigins.length > 0)\n this.allowedOrigins = options.allowedOrigins.concat(this.allowedOrigins);\n\n /**\n * Maximum sandboxes allowed to work at a given time.\n * @type {number}\n */\n this.maxWorkingSandboxes = options.maxWorkingSandboxes || 1;\n\n /** @type {number} */\n this.defaultMaxGPUs = 1;\n // this.GPUsAssigned = 0;\n \n // Object.defineProperty(this, 'GPUsAssigned', {\n // get: () => this.workingSandboxes.filter(sb => !!sb.requiresGPU).length,\n // enumerable: true,\n // configurable: false,\n // });\n\n /** @type {boolean} */\n this.isFetchingNewWork = false;\n /**\n * TODO: Remove this when the supervisor sends all of the sandbox\n * capabilities to the scheduler when fetching work.\n * @type {object}\n */\n this.capabilities = null;\n\n // This sets an offset into the watchdog bin at which to fire the sweeper\n /** @deprecated - UNUSED */\n this.watchdogSlotTime = options.watchdogInterval || tuning.watchdogInterval * 1000;\n /** @deprecated - UNUSED */\n this.watchdogOffset = Math.random();\n /** @deprecated - UNUSED */\n this.watchdogTimeout = null;\n\n /** @type {number} */\n this.lastProgressReport = 0;\n\n // @hack - dcp-env.isBrowserPlatform is not set unless the platform is _explicitly_ set,\n // using the default detected platform doesn't set it.\n // Fixing that causes an error in the wallet module's startup on web platform, which I\n // probably can't fix in a reasonable time this morning.\n // ~ER2020-02-20\n\n if (!options.maxWorkingSandboxes\n && DCP_ENV.browserPlatformList.includes(DCP_ENV.platform)\n && navigator.hardwareConcurrency > 1) {\n this.maxWorkingSandboxes = navigator.hardwareConcurrency - 1;\n if (typeof navigator.userAgent === 'string') {\n if (/(Android).*(Chrome|Chromium)/.exec(navigator.userAgent)) {\n this.maxWorkingSandboxes = 1;\n console.log('Doing work with Chromimum browsers on Android is currently limited to one sandbox');\n }\n }\n }\n\n /** @type {SupervisorCache} */\n this.cache = new SupervisorCache(this);\n /** @type {object} */\n this._connections = {}; /* active DCPv4 connections */\n // Call the watchdog every 7 seconds.\n this.watchdogInterval = setInterval(() => this.watchdog(), 7000);\n\n const ceci = this;\n\n // Initialize to null so these properties are recognized for the Supervisor class\n this.taskDistributorConnection = null;\n this.eventRouterConnection = null;\n this.resultSubmitterConnection = null;\n this.packageManagerConnection = null;\n this.openTaskDistributorConn = function openTaskDistributorConn()\n {\n let config = dcpConfig.scheduler.services.taskDistributor;\n ceci.taskDistributorConnection = new protocolV4.Connection(config, ceci.identityKeystore, connectionOptions(config.location, 'taskDistributor'));\n ceci.taskDistributorConnection.on('close', ceci.openTaskDistributorConn);\n }\n\n this.openEventRouterConn = function openEventRouterConn()\n {\n let config = dcpConfig.scheduler.services.eventRouter;\n ceci.eventRouterConnection = new protocolV4.Connection(config, ceci.identityKeystore, connectionOptions(config.location, 'eventRouter'));\n ceci.eventRouterConnection.on('close', ceci.openEventRouterConn);\n }\n \n this.openResultSubmitterConn = function openResultSubmitterConn()\n {\n let config = dcpConfig.scheduler.services.resultSubmitter;\n ceci.resultSubmitterConnection = new protocolV4.Connection(config, ceci.identityKeystore, connectionOptions(config.location, 'resultSubmitter'));\n ceci.resultSubmitterConnection.on('close', ceci.openResultSubmitterConn);\n }\n\n this.openPackageManagerConn = function openPackageManagerConn()\n {\n let config = dcpConfig.packageManager;\n ceci.packageManagerConnection = new protocolV4.Connection(config, ceci.identityKeystore, connectionOptions(config.location, 'packageManager'));\n ceci.packageManagerConnection.on('close', ceci.openPackageManagerConn);\n }\n }\n\n /**\n * Return worker opaqueId.\n * @type {opaqueId}\n */\n get workerOpaqueId() {\n if (!this._workerOpaqueId)\n this._workerOpaqueId = localStorage.getItem('workerOpaqueId');\n\n if (!this._workerOpaqueId || this._workerOpaqueId.length !== constants.workerIdLength) {\n this._workerOpaqueId = generateOpaqueId();\n localStorage.setItem('workerOpaqueId', this._workerOpaqueId);\n }\n\n return this._workerOpaqueId;\n }\n\n /**\n * This getter is the absolute source-of-truth for what the\n * identity keystore is for this instance of the Supervisor.\n */\n get identityKeystore() {\n assert(this.defaultIdentityKeystore);\n\n return this._identityKeystore || this.defaultIdentityKeystore;\n }\n\n /**\n * Open all connections. Used when supervisor is instantiated or stopped/started\n * to initially open connections.\n */\n instantiateAllConnections() {\n if (!this.taskDistributorConnection)\n this.openTaskDistributorConn();\n \n if (!this.eventRouterConnection)\n this.openEventRouterConn();\n \n if (!this.resultSubmitterConnection)\n this.openResultSubmitterConn();\n\n if (!this.packageManagerConnection)\n this.openPackageManagerConn();\n }\n\n /** Set the default identity keystore -- needs to happen before anything that talks\n * to the scheduler for work gets called. This is a wart and should be removed by\n * refactoring.\n *\n * The default identity keystore will be used if the Supervisor was not provided\n * with an alternate. This keystore will be located via the Wallet API, and \n * if not found, a randomized default identity will be generated. \n *\n * @param {object} ks An instance of wallet::Keystore -- if undefined, we pick the best default we can.\n * @returns {Promise<void>}\n */\n async setDefaultIdentityKeystore(ks) {\n try {\n if (ks) {\n this.defaultIdentityKeystore = ks;\n return;\n }\n\n if (this.defaultIdentityKeystore)\n return;\n\n try {\n this.defaultIdentityKeystore = await wallet.getId();\n } catch(e) {\n debugging('supervisor') && console.debug('supervisor: generating default identity', this.defaultIdentityKeystore.address);\n this.defaultIdentityKeystore = await new wallet.IdKeystore(null, '');\n }\n } finally {\n debugging('supervisor') && console.debug('supervisor: set default identity =', this.defaultIdentityKeystore.address);\n }\n }\n \n //\n // What follows is a bunch of utility functions for creating filtered views\n // of the slices and sandboxes array.\n //\n\n /**\n * Sandboxes that are in WORKING state.\n * Warning: Do not rely on this information being 100% accurate -- it may change in the next instant.\n * @type {Sandbox[]}\n */\n get workingSandboxes() {\n return this.sandboxes.filter(sandbox => sandbox.isWorking);\n }\n\n /**\n * This property is used as the target number of sandboxes to be associated with slices and transition sandbox-state from ASSIGNED -> WORKING.\n *\n * It is used in this.watchdog as to prevent a call to this.work when unallocatedSpace <= 0.\n * It is also used in this.distributeQueuedSlices where it is passed as an argument to this.matchSlicesWithSandboxes to indicate how many sandboxes\n * to associate with slices and transition sandbox-state from ASSIGNED -> WORKING.\n *\n * Warning: Do not rely on this information being 100% accurate -- it may change in the next instant.\n * @type {number}\n */\n get unallocatedSpace() {\n return this.maxWorkingSandboxes - this.sandboxes.filter(sandbox => sandbox.allocated).length;\n }\n\n /**\n * Slices that have SLICE_STATUS_UNASSIGNED status.\n * Warning: Do not rely on this information being 100% accurate -- it may change in the next instant.\n * @type {Slice[]}\n */\n get snapshotOfQueuedSlices() {\n return this.slices.filter(slice => slice.isUnassigned);\n }\n\n /**\n * Slices that have SLICE_STATUS_WORKING status.\n * Warning: Do not rely on this information being 100% accurate -- it may change in the next instant.\n * @type {Slice[]}\n */\n get snapshotOfWorkingSlices() {\n return this.slices.filter(slice => slice.isWorking)\n }\n\n /**\n * Sandboxes that are in READY_FOR_ASSIGN state.\n * Warning: Do not rely on this information being 100% accurate -- it may change in the next instant.\n * @type {Sandbox[]}\n */\n get snapshotOfReadiedSandboxes() {\n return this.sandboxes.filter(sandbox => sandbox.isReadyForAssign);\n }\n\n /**\n * Sandboxes that are in ASSIGNED state.\n * Warning: Do not rely on this information being 100% accurate -- it may change in the next instant.\n * @type {Sandbox[]}\n */\n get snapshotOfAssignedSandboxes() {\n return this.sandboxes.filter(sandbox => sandbox.isAssigned);\n }\n\n /**\n * Sandboxes that are in TERMINATED state.\n * Warning: Do not rely on this information being 100% accurate -- it may change in the next instant.\n * @type {Sandbox[]}\n */\n get snapshotOfTerminatedSandboxes() {\n return this.sandboxes.filter(sandbox => sandbox.isTerminated);\n }\n\n /**\n * Remove element from theArray.\n * @parameter {Array<*>} theArray\n * @parameter {object|number} element\n * @parameter {boolean} [assertExists = true]\n */\n removeElement(theArray, element, assertExists = false) {\n let index = theArray.indexOf(element);\n assert(index !== -1 || !assertExists);\n if (index !== -1) theArray.splice(index, 1);\n }\n\n /**\n * Find the authorization message wrt jobAddress and sliceNumber.\n * @parameter {string} text\n * @parameter {opaqueId} jobAddress\n * @parameter {number} sliceNumber\n * @returns {SignedAuthorizationMessageObject}\n */\n findAuthorizationMessage(text, jobAddress, sliceNumber) {\n const slice = this.slices.find(s => s.jobAddress === jobAddress && s.sliceNumber === sliceNumber);\n if (slice && slice.authorizationMessage)\n return slice.authorizationMessage;\n else\n throw new DCPError(`Undefined authorization. ${jobAddress}, ${sliceNumber}`, 'NOAUTH')\n }\n\n /**\n * If the elements of sandboxSliceArray are not unique, log the duplicates and dump the array.\n * @parameter {SandboxSlice[]} sandboxSliceArray\n * @parameter {string} header\n */\n dumpSandboxSlicesIfNotUnique(sandboxSliceArray, header) {\n if (!this.isUniqueSandboxSlices(sandboxSliceArray, header))\n this.dumpSandboxSlices(sandboxSliceArray);\n }\n\n /**\n * Log sandboxSlice.\n * @parameter {SandboxSlice} sandboxSlice\n * @returns {string}\n */\n dumpSandboxSlice(sandboxSlice) {\n return `${sandboxSlice.sandbox.id}-${sandboxSlice.slice.sliceNumber}.${sandboxSlice.slice.jobAddress}`;\n }\n\n /**\n * Log { sandbox, slice }.\n * @parameter {Sandbox} sandbox\n * @parameter {Slice} slice\n * @returns {string}\n */\n dumpSandboxAndSlice(sandbox, slice) {\n return this.dumpSandboxSlice({ sandbox, slice });\n }\n\n /**\n * Dump sandboxSliceArray.\n * @parameter {SandboxSlice[]} sandboxSliceArray\n * @parameter {string} header\n */\n dumpSandboxSlices(sandboxSliceArray, header) {\n if (header) console.log(`\\n${header}`);\n sandboxSliceArray.forEach(x => console.log(`\\t{ ${x.sandbox.id}.${x.sandbox.jobAddress}.${x.sandbox.state}, ${x.slice.sliceNumber}.${x.slice.jobAddress}.${x.slice.status} }`));\n }\n\n /**\n * Check sandboxSliceArray for duplicates.\n * @parameter {SandboxSlice[]} sandboxSliceArray\n * @parameter {string} header\n * @parameter {function} log\n * @returns {boolean}\n */\n isUniqueSandboxSlices(sandboxSliceArray, header, log) {\n return sandboxSliceArray.length === this.makeUniqueSandboxSlices(sandboxSliceArray, header, log).length;\n }\n\n /**\n * Returns a copy of sandboxSliceArray with all duplicates removed.\n * @parameter {SandboxSlice[]} sandboxSliceArray\n * @parameter {string} header\n * @parameter {function} log\n * @returns {SandboxSlice[]}\n */\n makeUniqueSandboxSlices(sandboxSliceArray, header, log) {\n const result = [], slices = [], sandboxes = [];\n let once = true;\n sandboxSliceArray.forEach(x => {\n const sliceIndex = slices.indexOf(x.slice);\n const sandboxIndex = sandboxes.indexOf(x.sandbox);\n\n if (sandboxIndex >= 0) {\n if (once && header) console.log(`\\n${header}`); once = false;\n log ? log(x.sandbox) : console.log(`\\tWarning: Found duplicate sandbox ${x.sandbox.id}.${x.sandbox.jobAddress}.${x.sandbox.state}.`);\n } else sandboxes.push(x.sandbox);\n\n if (sliceIndex >= 0) {\n if (once && header) console.log(`\\n${header}`); once = false;\n log ? log(x.slice) : console.log(`\\tWarning: Found duplicate slice ${x.slice.sliceNumber}.${x.slice.jobAddress}.${x.slice.status}.`);\n } else {\n slices.push(x.slice);\n if (sandboxIndex < 0) result.push(x);\n }\n });\n return result;\n }\n\n /** NOT used ATM, it's faster to just filter the assigned sandboxes for a jobAddress on-demand\n * \n * Bins the assigned sandboxes in an object, keyed by their jobAddress.\n * { 0xgenAddress1: sandboxes[], 0xgenAddress2: sandboxes[], ... }\n * @type {object}\n */\n get assignedSandboxesSorted () {\n return this.assignedSandboxes.reduce((o, w) => {\n if (!w.jobAddress) throw new Error(\"Assigned sandbox doesn't have a job opaque id\", w);\n\n if (w.jobAddress in o) {\n o[w.jobAddress].push(w);\n } else o[w.jobAddress] = [w];\n\n return o;\n }, {});\n }\n\n /**\n * Attempts to create and start a given number of sandboxes.\n * The sandboxes that are created can then be assigned for a\n * specific job at a later time. All created sandboxes\n * get put into the @this.readiedSandboxes array.\n *\n * @param {number} numSandboxes - the number of sandboxes to create\n * @returns {Promise<Sandbox[]>} - resolves with array of created sandboxes, rejects otherwise\n * @throws when given a numSandboxes is not a number > 0 or if numSandboxes is Infinity\n */\n async readySandboxes (numSandboxes) {\n debug('Readying sandboxes');\n if (typeof numSandboxes !== 'number' || Number.isNaN(numSandboxes) || numSandboxes === Infinity) {\n throw new Error(`${numSandboxes} is not a number of sandboxes that can be readied.`);\n }\n if (numSandboxes <= 0) {\n return [];\n }\n\n const sandboxStartPromises = [];\n const sandboxes = [];\n const errors = [];\n for (let i = 0; i < numSandboxes; i++) {\n const sandbox = new Sandbox(this.cache, {\n ...this.options.sandboxOptions,\n }, this.allowedOrigins);\n sandbox.addListener('ready', () => this.emit('sandboxReady', sandbox));\n sandbox.addListener('start', () => {\n this.emit('sandboxStart', sandbox);\n\n // When sliceNumber == 0, result-submitter status skips the slice,\n // so don't send it in the first place.\n // The 'start' event is fired when a worker starts up, hence there's no way\n // to determine whether sandbox has a valid slice without checking.\n if (sandbox.slice) {\n const jobAddress = sandbox.jobAddress;\n const sliceNumber = sandbox.slice.sliceNumber;\n const authorizationMessage = this.findAuthorizationMessage('Supervisor.readySandboxes sandboxStart:', jobAddress, sliceNumber);\n\n this.resultSubmitterConnection.send('status', {\n worker: this.workerOpaqueId,\n slices: [{\n job: jobAddress,\n sliceNumber: sliceNumber,\n status: 'begin',\n authorizationMessage,\n }],\n });\n }\n });\n sandbox.addListener('workEmit', ({ eventName, payload }) => {\n // Need to check if the sandbox hasn't been assigned a slice yet.\n if (!sandbox.slice) {\n if (Supervisor.debugBuild) {\n console.error(\n `Sandbox not assigned a slice before sending workEmit message to scheduler. 'workEmit' event originates from \"${eventName}\" event`, \n payload,\n );\n }\n }\n else\n {\n let resultP;\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 const authorizationMessage = this.findAuthorizationMessage('Supervisor.readySandboxes workEmit:', jobAddress, sliceNumber);\n \n if (!authorizationMessage)\n {\n console.warn(`workEmit: missing authorization message for job ${jobAddress}, slice: ${sliceNumber}`);\n return;\n }\n \n resultP = this.eventRouterConnection.send('workEmit', {\n eventName,\n payload,\n job: jobAddress,\n slice: sliceNumber,\n worker: this.workerOpaqueId,\n authorizationMessage,\n }).catch(error => {\n console.warn(`workEmit: unable to send message to event router ${error.message}`);\n if (Supervisor.debugBuild)\n console.error('workEmit error:', error);\n });\n\n if (Supervisor.debugBuild)\n resultP.then(result => {\n if (!result || !result.success)\n {\n console.warn('workEmit: event router did not accept event');\n if (Supervisor.debugBuild)\n console.error('workEmit result:', result);\n }\n });\n }\n });\n\n // When any sbx completes, \n sandbox.addListener('complete', () => {\n // this.distributeQueuedSlices();\n this.watchdog();\n //this.work();\n });\n\n sandbox.on('sandboxError', (error) => handleSandboxError(this, sandbox, error));\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.map(sandbox => sandbox.isTerminated);\n terminatedSandboxes = terminatedSandboxes.filter(s => s === true);\n if(terminatedSandboxes.length === this.sandboxes.length) {\n await this.readySandboxes(1);\n \n if(this.sandboxes.length !== terminatedSandboxes.length + 1) // if cannot create a new sandbox\n {\n this.returnSlices(this.queuedSlices).then(() => {\n // Kill corresponding entries in this.slices.\n this.queuedSlices.forEach(slice => {\n this.removeElement(this.slices, slice);\n });\n this.queuedSlices.length = 0;\n });\n }\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 this.sandboxes.push(sandbox);\n sandboxes.push(sandbox);\n })\n .catch((err) => {\n errors.push(err);\n this.returnSandbox(sandbox);\n if(err.code === 'ENOWORKER'){\n throw new DCPError(\"Cannot use localExec without dcp-worker installed. Use the command 'npm install dcp-worker' to install the neccessary modules.\", 'ENOWORKER');\n }\n }));\n }\n\n await Promise.all(sandboxStartPromises);\n\n if (errors.length) {\n console.warn(`Failed to ready ${errors.length} of ${numSandboxes} sandboxes.`, errors);\n throw new Error('Failed to ready sandboxes.');\n }\n\n if (this.readiedSandboxes.length > 0)\n sandboxes.push(...this.readiedSandboxes);\n this.readiedSandboxes = sandboxes;\n\n debugging() && console.log(`readySandboxes: all readied sandboxes ${JSON.stringify(this.readiedSandboxes.map(sandbox => sandbox.id))}`);\n \n return sandboxes;\n }\n\n /**\n * Accepts a sandbox after it has finished working or encounters an error.\n * If the sandbox was terminated or if \"!slice || slice.failed\" then\n * the sandbox will be removed from the sandboxes array and terminated if necessary.\n * Otherwise it will try to distribute a slice to the sandbox immediately.\n *\n * @param {Sandbox} sandbox - the sandbox to return\n * @param {Slice} [slice] - the slice just worked on; !slice => terminate\n */\n returnSandbox (sandbox, slice) {\n if (!slice || slice.failed || sandbox.isTerminated) {\n this.removeElement(this.sandboxes, sandbox);\n if (!sandbox.isTerminated) {\n sandbox.terminate(false);\n }\n }\n }\n\n /**\n * Terminates sandboxes, in order of creation, when the total started sandboxes exceeds the total allowed sandboxes.\n *\n * @todo Check through the currently started sandboxes and attempt to terminate\n * ones that aren't frequently used.\n * ^^^ This todo may be partially done due to terminating older sandboxes - RR Nov 2019\n * @returns {Promise<void>}\n */\n pruneSandboxes () {\n let numOver = this.sandboxes.length - (dcpConfig.worker.maxAllowedSandboxes + this.maxWorkingSandboxes);\n if (numOver <= 0) return;\n \n // Don't kill readied sandboxes while creating readied sandboxes.\n if (!this.doNotPrune) {\n for (let index = 0; index < this.readiedSandboxes.length; ) {\n const sandbox = this.readiedSandboxes[index];\n // If the sandbox is allocated, advance to the next one in the list.\n if (sandbox.allocated) {\n index++;\n continue;\n }\n // Otherwise, remove this sandbox but look at the same array index in the next loop.\n debugging() && console.log(`pruneSandboxes: Killing readied sandbox ${sandbox.id}`);\n this.readiedSandboxes.splice(index, 1);\n this.returnSandbox(sandbox);\n\n if (--numOver <= 0) break;\n }\n }\n\n if (numOver <= 0) return;\n for (let index = 0; index < this.assignedSandboxes.length; ) {\n const sandbox = this.assignedSandboxes[index];\n // If the sandbox is allocated, advance to the next one in the list.\n if (sandbox.allocated) {\n index++;\n continue;\n }\n // Otherwise, remove this sandbox but look at the same array index in the next loop.\n debugging() && console.log(`pruneSandboxes: Killing assigned sandbox ${sandbox.id}`);\n this.assignedSandboxes.splice(index, 1);\n this.returnSandbox(sandbox);\n\n if (--numOver <= 0) break;\n }\n }\n \n /**\n * Basic watch dog to check if there are idle sandboxes and\n * attempts to nudge the supervisor to feed them work.\n *\n * Run in an interval created in @constructor .\n * @returns {Promise<void>}\n */\n async watchdog () {\n if (!this.watchdogState)\n this.watchdogState = {};\n \n // Every 5 minutes, report progress of all working slices to the scheduler\n if (Date.now() > ((this.lastProgressReport || 0) + sandboxTuning.progressReportInterval)) {\n // console.log('454: Assembling progress update...');\n this.lastProgressReport = Date.now();\n \n const slices = [];\n \n this.queuedSlices.forEach(slice => {\n const jobAddress = slice.jobAddress;\n const sliceNumber = slice.sliceNumber;\n const authorizationMessage = this.findAuthorizationMessage('Supervisor.watchdog:', jobAddress, sliceNumber);\n\n if (authorizationMessage) {\n addToSlicePayload(slices, jobAddress, sliceNumber, sliceStatus.scheduled, authorizationMessage);\n }\n })\n \n this.workingSandboxes.forEach(sb => {\n // The lifetime of sandbox.isWorking is (or should be) precisely defined,\n // so we can assume sb.slice is always valid here.\n assert(sb.slice);\n\n const jobAddress = sb.jobAddress;\n const sliceNumber = sb.slice.sliceNumber;\n const authorizationMessage = this.findAuthorizationMessage('Supervisor.watchdog:', jobAddress, sliceNumber);\n\n if (authorizationMessage) {\n addToSlicePayload(slices, jobAddress, sliceNumber, 'progress', authorizationMessage);\n }\n });\n\n if (slices.length) {\n // console.log('471: sending progress update...');\n const progressReportPayload = {\n worker: this.workerOpaqueId,\n slices,\n };\n\n this.resultSubmitterConnection.send('status', progressReportPayload)\n .catch(error => {\n console.error('479: Failed to send status update:', error/*.message*/);\n });\n }\n }\n\n if (this.worker.working) {\n if (this.unallocatedSpace > 0) {\n await this.work().catch(err => {\n if (!this.watchdogState[err.code || '0'])\n this.watchdogState[err.code || '0'] = 0;\n if (Date.now() - this.watchdogState[err.code || '0'] > ((dcpConfig.worker.watchdogLogInterval * timeDilation || 120) * 1000))\n console.error('301: Failed to start work:', err);\n this.watchdogState[err.code || '0'] = Date.now();\n });\n }\n\n this.pruneSandboxes();\n }\n }\n\n /**\n * Gets the logical and physical number of cores and also\n * the total number of sandboxes the worker is allowed to run\n *\n */\n getStatisticsCPU() {\n if (DCP_ENV.isBrowserPlatform) {\n return {\n worker: this.workerOpaqueId,\n lCores: window.navigator.hardwareConcurrency,\n pCores: dcpConfig.worker.pCores || window.navigator.hardwareConcurrency,\n sandbox: this.maxWorkingSandboxes\n }\n }\n\n return {\n worker: this.workerOpaqueId,\n lCores: requireNative('os').cpus().length,\n pCores: requireNative('physical-cpu-count'),\n sandbox: this.maxWorkingSandboxes\n }\n }\n\n /**\n * Call to start doing work on the network.\n * This is the one place where requests to fetch new slices are made.\n * After the initial slice and job are fetched it calls @this.distributeQueuedSlices.\n *\n * @returns {Promise<void>}\n */\n async work()\n {\n await this.setDefaultIdentityKeystore();\n\n // instantiate connections that don't exist.\n this.instantiateAllConnections();\n\n let slicesToFetch;\n if (this.options.priorityOnly && this.options.jobAddresses.length === 0) {\n slicesToFetch = 0;\n } else if (this.queuedSlices.length > 1) {\n // We have slices queued, no need to fetch\n slicesToFetch = 0;\n } else {\n // The queue is almost empty (there may be 0 or 1 element), fetch a full task.\n // The task is full, in the sense that it will contain slices whose\n // aggregate execution time is this.maxWorkingSandboxes * 5-minutes.\n // However, there can only be this.unallocatedSpace # of long slices.\n // Thus we need to know whether a slice in this.queuedSlices is long or not.\n // (A long slice has estimated execution time > 5-minutes.)\n const longSliceCount = (this.queuedSlices.length > 0 && this.queuedSlices[0].isLongSlice) ? 1 : 0;\n slicesToFetch = this.unallocatedSpace - longSliceCount;\n }\n\n debugging() && console.log(`Supervisor.work: Try to get ${slicesToFetch} slices in working sandboxes, unallocatedSpace ${this.unallocatedSpace}, queued slices ${this.queuedSlices.length}; isFetchingNewWork: ${this.isFetchingNewWork}`);\n \n // Fetch a new task if we have no more slices queued, then start workers\n try {\n if (slicesToFetch > 0 && !this.isFetchingNewWork) {\n this.isFetchingNewWork = true;\n\n /**\n * This will only ready sandboxes up to a total count of\n * maxWorkingSandboxes (in any state). It is not possible to know the\n * actual number of sandboxes required until we have the slices because we\n * may have sandboxes assigned for the slice's job already.\n *\n * If the evaluator cannot start (ie. if the evalServer is not running),\n * then the while loop will keep retrying until the evalServer comes online\n */\n while (this.maxWorkingSandboxes > this.sandboxes.length) {\n await this.readySandboxes(1)\n .then(() => this.readySandboxes(this.maxWorkingSandboxes - this.sandboxes.length))\n .catch (error => {\n console.warn(`906: failed to ready sandboxes; will retry`, error.code, error.message);\n });\n }\n\n /**\n * Temporary change: Assign the capabilities of one of readied sandboxes\n * before fetching slices from the scheduler.\n *\n * TODO: Remove this once fetchTask uses the capabilities of every\n * sandbox to fetch slices.\n */\n if (!this.capabilities) {\n this.capabilities = this.sandboxes[0].capabilities;\n this.emit('capabilitiesCalculated', this.capabilities);\n }\n\n if(DCP_ENV.isBrowserPlatform && this.capabilities.browser)\n this.capabilities.browser.chrome = DCP_ENV.isBrowserChrome;\n\n const fetchTimeout = setTimeout(() => {\n console.warn(`679: Fetch exceeded timeout, will reconnect at next watchdog interval`);\n \n this.taskDistributorConnection.close('Fetch timed out', Math.random() > 0.5).catch(error => {\n console.error(`931: Failed to close task-distributor connection`, error);\n });\n this.resultSumitterConnection.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 (error)\n {\n console.error('Failed to connect to result submitter, refusing to fetch slices. Will try again at next fetch cycle.')\n debugging('supervisor') && console.log(`Error: ${error}`);\n this.isFetchingNewWork = false; // <-- done in the `finally` block, below\n clearTimeout(fetchTimeout);\n this.taskDistributorConnection.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;\n }\n await this.fetchTask(slicesToFetch).finally(() => {\n clearTimeout(fetchTimeout);\n this.isFetchingNewWork = false;\n });\n }\n \n this.distributeQueuedSlices().then(() => debugging('supervisor') && 'supervisor: finished distributeQueuedSlices()').catch((e)=>console.error(e)) ;\n// await this.distributeQueuedSlices();\n // No catch(), because it will bubble outward to the caller\n } finally {\n \n }\n }\n\n /**\n * Generate the workerComputeGroups property of the requestTask message. \n * \n * Concatenate the compute groups object from dcpConfig with the list of compute groups\n * from the supervisor, and remove the public group if accidentally present. Finally,\n * we transform joinSecrets/joinHash into joinHashHash for secure transmission.\n *\n * @note computeGroup objects with joinSecrets are mutated to record their hashes. This\n * affects the supervisor options and dcpConfig. Re-adding a joinSecret property\n * to one of these will cause the hash to be recomputed.\n */\n generateWorkerComputeGroups()\n {\n var computeGroups = Object.values(dcpConfig.worker.computeGroups || {});\n if (this.options.computeGroups)\n computeGroups = computeGroups.concat(this.options.computeGroups);\n computeGroups = computeGroups.filter(group => group.id !== constants.computeGroups.public.id);\n const hashedComputeGroups = [];\n for (const group of computeGroups)\n {\n const groupCopy = Object.assign({}, group);\n if ((group.joinSecret || group.joinHash) && (!group.joinHashHash || this.lastDcpsid !== this.taskDistributorConnection.dcpsid))\n {\n let joinHash;\n if(group.joinHash) {\n joinHash = group.joinHash.replace(/\\s+/g, ''); // strip whitespace\n } else {\n joinHash = calculateJoinHash(groupCopy);\n } \n\n groupCopy.joinHashHash = hash.calculate(hash.eh1, joinHash, this.taskDistributorConnection.dcpsid);\n delete groupCopy.joinSecret;\n delete groupCopy.joinHash;\n debugging('computeGroups') && console.debug(`Calculated joinHash=${joinHash} for`, groupCopy);\n }\n hashedComputeGroups.push(groupCopy);\n }\n this.lastDcpsid = this.taskDistributorConnection.dcpsid;\n debugging('computeGroups') && console.debug('Requesting ', computeGroups.length, 'non-public groups for session', this.lastDcpsid);\n return hashedComputeGroups;\n }\n\n /**\n * Fetches a task, which contains job information and slices for sandboxes and\n * manages events related to fetching tasks so the UI can more clearly display\n * to user what is actually happening.\n * @param {number} numCores\n * @returns {Promise<void>} The requestTask request, resolve on success, rejects otherwise.\n * @emits Supervisor#fetchingTask\n * @emits Supervisor#fetchedTask\n */\n async fetchTask(numCores)\n {\n this.emit('fetchingTask');\n debugging('supervisor') && console.debug('supervisor: fetching task');\n const requestPayload = {\n numCores,\n coreStats: this.getStatisticsCPU(),\n numGPUs: this.defaultMaxGPUs,\n capabilities: this.capabilities,\n paymentAddress: this.paymentAddress,\n jobAddresses: this.options.jobAddresses, // when set, only fetches slices for these jobs\n localExec: this.options.localExec,\n workerComputeGroups: this.generateWorkerComputeGroups(),\n minimumWage: dcpConfig.worker.minimumWage || this.options.minimumWage,\n readyJobs: [ /* list of jobs addresses XXXwg */ ],\n previouslyWorkedJobs: this.cache.jobs,\n };\n\n // workers should be part of the public compute group by default\n if (!booley(dcpConfig.worker.leavePublicGroup) && !booley(this.options.leavePublicGroup))\n requestPayload.workerComputeGroups.push(constants.computeGroups.public);\n debugging('computeGroups') && console.log(`Fetching work for ${requestPayload.workerComputeGroups.length} ComputeGroups: `, requestPayload.workerComputeGroups);\n \n debugging() && console.log(`${shortTime()} fetchTask wants ${numCores} slices, unallocatedSpace ${this.unallocatedSpace}, queuedSlices ${this.queuedSlices.length}`);\n try {\n debugging('requestTask') && debug('requestPayload', requestPayload);\n\n let result = await this.taskDistributorConnection.send('requestTask', requestPayload);\n let responsePayload = result.payload; \n \n if (!result.success) { \n debugging() && console.log('Task fetch failure; request=', requestPayload);\n debugging() && console.log('Task fetch failure; response=', result.payload);\n throw new DCPError('Unable to fetch task for worker', responsePayload);\n }\n\n const sliceCount = responsePayload.body.task.length || 0;\n\n /**\n * The fetchedTask event fires when the supervisor has finished trying to\n * fetch work from the scheduler (task-manager). The data emitted is the\n * number of new slices to work on in the fetched task.\n *\n * @event Supervisor#fetchedTask\n * @type {number}\n */\n this.emit('fetchedTask', sliceCount);\n\n if (sliceCount < 1) {\n return;\n }\n\n /*\n * DCP-1698 Send auth msg with tasks to worker, then validate authority of worker to send slice info back to scheduler.\n * payload structure: { owner: this.address, signature: signature, auth: messageLightWeight, body: messageBody };\n * XXXpfr: @todo Do not think we need 'owner', think about it.\n */\n const { body, ...authorizationMessage } = responsePayload;\n const { newJobs, task } = body;\n assert(newJobs); // It should not be possible to have !newJobs -- we throw on !success.\n\n /* Delete all jobs in the supervisorCache that were not represented in this task\n * or there is no sandbox assign to these jobs.\n */\n let cachedJobs = this.cache.jobs;\n let allocatedSandbox = this.sandboxes.filter(sandbox => sandbox.allocated);\n let assignedJob = allocatedSandbox.map(sandbox => {\n if(sandbox.jobAddress) return sandbox.jobAddress;\n })\n let jobAddressToBeInCache = Object.keys(newJobs);\n assignedJob.forEach(jobAddress => {\n if (jobAddressToBeInCache.indexOf(jobAddress) === -1) jobAddressToBeInCache.push(jobAddress);\n });\n for (let i = 0; i < cachedJobs.length; i++) {\n let cachedJobAddress = cachedJobs[i];\n if (!(jobAddressToBeInCache.includes(cachedJobAddress))) {\n this.cache.remove('job', cachedJobAddress);\n }\n }\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 debugging('supervisor') && console.log(`${shortTime()} fetchTask: Task has ${task.length} slices from ${Object.keys(newJobs).length} new jobs.`);\n\n const tmpQueuedSlices = [];\n for (const taskElement of task) {\n // Memoize authMessage onto the Slice object, this should\n // follow it for its entire life in the worker\n const slice = new Slice(taskElement);\n slice.authorizationMessage = authorizationMessage;\n\n tmpQueuedSlices.push(slice);\n debugging('supervisor') && console.log(`/t${shortTime()} fetchTask: slice ${taskElement.sliceNumber}, jobAddress ${taskElement.jobAddress}`);\n }\n\n /** XXXpfr @todo Try to get rid of this.slices. */\n this.slices.push(...tmpQueuedSlices);\n\n this.queuedSlices = [...tmpQueuedSlices, ...this.queuedSlices]; /* Make sure the old ones are up front. */\n\n } catch (error) {\n this.emit('fetchTaskFailed', error);\n debugging('supervisor') && console.debug(`Supervisor.fetchTask failed!: error: ${error}`);\n }\n }\n\n /**\n * For each slice in this.queuedSlices, match with a sandbox in the following order:\n * 1. Try to find an already assigned sandbox in this.assignedSandboxes for the slice's job.\n * 2. Find a ready sandbox in this.readiedSandboxes that is unassigned.\n * 3. Ready a new sandbox and use that.\n *\n * Take great care in assuring sandboxes and slices are uniquely associated, viz.,\n * a given slice cannot be associated with multiple sandboxes and a given sandbox cannot be associated with multiple slices.\n * The lack of such uniqueness has been the root cause of several difficult bugs.\n *\n * @param {number} numSlices The number of slices to match with sandboxes.\n * @returns {Promise<SandboxSlice[]>} Returns SandboxSlice[], may have length zero.\n */\n async matchSlicesWithSandboxes (numSlices) {\n const sandboxSlices = [];\n if (numSlices <= 0 || this.queuedSlices.length == 0 || this.matching)\n return sandboxSlices;\n\n // Don't ask for more than we have.\n if (numSlices > this.queuedSlices.length)\n numSlices = this.queuedSlices.length;\n\n debugging() && console.log(`${shortTime()} matchSlicesWithSandboxes: numSlices ${numSlices}, queued slices ${this.queuedSlices.length}: assigned ${this.assignedSandboxes.length}, readied ${this.readiedSandboxes.length}, total sandbox count: ${this.sandboxes.length}`);\n\n // Auxiliary function to mark sandboxes as allocated so they cannot be pruned out from under us.\n function markAsAllocated(sandboxArray, numMarked) {\n if (numMarked > sandboxArray.length) numMarked = sandboxArray.length;\n for (let k = 1; k <= numMarked; k++) {\n assert(!sandboxArray[sandboxArray.length - k].allocated);\n sandboxArray[sandboxArray.length - k].allocated = true;\n }\n }\n\n // Filter and sandboxes that are not ready out of the `readiedSandboxes` list. Ie filter\n // out sandboxes that have been terminated before being used.\n this.readiedSandboxes = this.readiedSandboxes.filter((sandbox) => sandbox.state === 'READY_FOR_ASSIGN')\n\n try\n {\n this.matching = true;\n const jobAssignedMap = {};\n\n // Until this is rock-solid-stable I want to check for uniquenees.\n if (true) {\n dumpSlicesIfNotUnique(this.queuedSlices, `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 if (debugging()) {\n dumpSlices(this.queuedSlices, 'matchSlicesWithSandboxes(top): this.queuedSlices');\n dumpSandboxes(this.assignedSandboxes, 'matchSlicesWithSandboxes(top): this.assignedSandboxes');\n dumpSandboxes(this.readiedSandboxes, 'matchSlicesWithSandboxes(top): this.readiedSandboxes');\n }\n\n let counter = 0; // General counter.\n let assignedCounter = 0; // How many assigned sandboxes are being used.\n let readyCounter = 0; // How many sandboxes used from the existing this.queuedSlices.\n let newCounter = 0; // How many sandboxes that needed to be newly created.\n\n //\n // The Ideas:\n // 1) We match each slice with a sandbox, in the order that they appear in this.queuedSlices.\n // This allows us to try different orderings of execution of slices. E.g. Wes suggested\n // trying to execute slices from different jobs with maximal job diversity -- specifically\n // if there are 3 jobs j1,j2,j3, with slices s11, s12 from j1, s21, s22, s23 from j2 and\n // s31, s32 from j3, then we try to schedule, in order s11, s21, s31, s12, s22, s32, s23.\n //\n // 2) Before matching slices with sandboxes, we allocate available assigned and readied sandboxes\n // and if more are needed then we create and allocate new ones.\n //\n // 3) Finally we match slices with sandboxes and return an array of sandboxSlice pairs.\n //\n \n //\n // General description of the complexity of promise races.\n // NOTE: Supervisor.returnSandbox does not call any promises and hence does not need to be async, so that has been dropped.\n // The added benefit is that Supervisor.pruneSandboxes is also not async anymore and hence there is better safety with the\n // race between Supervisor.pruneSandboxes and Supervisor.readySandboxes. Specifically, during pruneSandboxes there are no\n // promise calls which could result in 'async readySandboxes' (which creates new 'readied' sandboxes) being called in the midst\n // of pruning 'readied' sandboxes. There is still a race in 'async matchSlicesWithSandboxes' when calling 'async readySandboxes'\n // where it is possible to interleave with 'async watchdog' which calls pruneSandboxes, but this race is prevented by setting\n // 'this.doNotPrune = true;' in 'async matchSlicesWithSandboxes' right before the call to 'async readySandboxes', because\n // 'if (!this.doNotPrune)' guards pruning 'readied' sandboxes.\n //\n\n // Associate assigned sandboxes for a given jobAddress with the corresponding count of slices owned\n // by the same jobAddress.\n for (let index = this.queuedSlices.length - 1; index >= 0; index--) {\n const slice = this.queuedSlices[index];\n assert(slice.isUnassigned);\n\n const jobAddress = slice.jobAddress.valueOf();\n let assignedState = jobAssignedMap[jobAddress];\n\n if (!assignedState) {\n // Get all assigned sandboxes for jobAddress.\n assignedState = { sandboxes: this.assignedSandboxes.filter(sb => sb.jobAddress.valueOf() === jobAddress), count: 1 };\n jobAssignedMap[jobAddress] = assignedState;\n } else {\n assignedState.count++;\n }\n\n if (++counter >= numSlices)\n break;\n }\n counter = 0;\n\n // Calculate how many new sandboxes are needed.\n for (const [jobAddress, assignedState] of Object.entries(jobAssignedMap)) {\n // Count the # of assigned sandboxes we can use.\n const assignedUsedCount = Math.min(assignedState.count, assignedState.sandboxes.length);\n assignedCounter += assignedUsedCount;\n\n // Mark the assigned sandboxes we can use as allocated, so they're not pruned out from under us.\n markAsAllocated(assignedState.sandboxes, assignedUsedCount);\n\n // Count slices w/o assigned sandbox.\n const count = Math.max(assignedState.count - assignedState.sandboxes.length, 0);\n counter += count;\n debugging() && console.log(`matchSlicesWithSandboxes: job ${jobAddress}, assigned ${assignedState.sandboxes.length}, slices ${assignedState.count}, need ${count}/${this.readiedSandboxes.length} readied sandboxes.`);\n }\n\n counter -= this.readiedSandboxes.length;\n if (counter > 0) { // Number of new sandboxes needed.\n newCounter = counter;\n readyCounter = this.readiedSandboxes.length;\n // Don't prune readied sandboxes while creating readied sandboxes -- yes, it happens...\n this.doNotPrune = true;\n try { await this.readySandboxes(newCounter); } finally { this.doNotPrune = false; }\n } else {\n readyCounter = this.readiedSandboxes.length + counter;\n }\n counter = 0;\n\n debugging() && console.log(`matchSlicesWithSandboxes: newCounter ${newCounter}, readyCounter ${readyCounter}, assignedCounter ${assignedCounter}, total readied ${this.readiedSandboxes.length}`);\n\n // Validate algorithm consistency.\n if (Supervisor.debugBuild && assignedCounter + readyCounter + newCounter !== numSlices) {\n // Structured assert.\n throw new DCPError(`matchSlicesWithSandboxes: Algorithm is corrupt ${assignedCounter} + ${readyCounter} + ${newCounter} !== ${numSlices}`);\n }\n\n // Mark the readied sandboxes we can use as allocated, so they're not pruned out from under us.\n // Note: We could run markAsAllocated before the async call to readySandboxes, which would gaurd from pruning,\n // then run it again on the brand new sandboxes just created, but it makes the code hard to read and isn't necessary,\n // beacause the doNotPrune trick seems to work. We also check if we unexpectedly run out of sandboxes (assert in debug.)\n markAsAllocated(this.readiedSandboxes, readyCounter + newCounter);\n\n while (this.queuedSlices.length > 0) {\n const slice = this.queuedSlices.pop();\n assert(slice.isUnassigned);\n\n const jobAddress = slice.jobAddress.valueOf();\n const assignedSandboxesForJob = jobAssignedMap[jobAddress].sandboxes;\n assert(assignedSandboxesForJob);\n\n if (assignedSandboxesForJob.length > 0) {\n const sandbox = assignedSandboxesForJob.pop();\n this.removeElement(this.assignedSandboxes, sandbox);\n assert(jobAddress === sandbox.jobAddress.valueOf());\n\n debugging() && console.log(`matchSlicesWithSandboxes: Assigned sandbox matched ${this.dumpSandboxAndSlice(sandbox, slice)}`);\n sandboxSlices.push({ sandbox, slice });\n } else {\n // There may be a race between creating and pruning, though it should be fixed with the doNotPrune trick.\n assert(this.readiedSandboxes.length > 0);\n if (this.readiedSandboxes.length > 0) {\n const sandbox = this.readiedSandboxes.pop();\n debugging() && console.log(`matchSlicesWithSandboxes: Readied sandbox matched ${this.dumpSandboxAndSlice(sandbox, slice)}`);\n sandboxSlices.push({ sandbox, slice });\n }\n }\n\n if (++counter >= numSlices)\n break;\n }\n\n debugging() && console.log(`matchSlicesWithSandboxes: Matches: ${ JSON.stringify(sandboxSlices.map(ss => this.dumpSandboxSlice(ss))) }`);\n\n // Until this is rock-solid-stable I want to check for uniquenees.\n if (true) {\n this.dumpSandboxSlicesIfNotUnique(sandboxSlices, `Warning: sandboxSlices; { sandbox, slice } pairs are not unique!`);\n }\n\n if (debugging()) {\n console.log(`matchSlicesWithSandboxes: found ${sandboxSlices.length} sandboxes for jobs ${JSON.stringify(Object.keys(jobAssignedMap))}: assigned ${assignedCounter}, ready ${readyCounter}, new ${newCounter}`);\n this.dumpSandboxSlices(sandboxSlices, 'matchSlicesWithSandboxes: sandboxSlices');\n dumpSandboxes(this.assignedSandboxes, 'matchSlicesWithSandboxes: this.assignedSandboxes');\n dumpSandboxes(this.readiedSandboxes, 'matchSlicesWithSandboxes: this.readiedSandboxes');\n console.log(`matchSlicesWithSandboxes: remaining readied: ${JSON.stringify(this.readiedSandboxes.map(s => s.id))}`);\n }\n\n } catch (e) {\n console.error('Error in matchSlicesWithSandboxes:', e);\n } finally {\n this.matching = false;\n }\n\n debugging() && console.log(`${shortTime()} matchSlicesWithSandboxes allocated ${sandboxSlices.length} sandboxes, queuedSlices ${this.queuedSlices.length}, unallocatedSpace ${this.unallocatedSpace}.`);\n\n return sandboxSlices;\n }\n\n /**\n * This method will call @this.startSandboxWork(sandbox, slice) for { sandbox, slice }\n * in the array returned by @this.matchSlicesWithSandboxes(availableSandboxes) until all sandboxes are working.\n * It is possible for a sandbox to finish simultaneously and leave a sandbox that is not working.\n * Note: @this.queuedSlices may be exhausted before all sandboxes are working.\n * @returns {Promise<void>}\n */\n async distributeQueuedSlices () {\n //\n // All matching of sandbox with slice is taken care of by @this.matchSlicesWithSandboxes().\n // We do not use @this.snapshotOfQueuedSlices becuase it is in a fuzzy state; when matched with a sandbox,\n // a slice transitions to working at an unknown point in the future.\n // Instead we use @this.queuedSlices which really is a queue, whose elements are\n // dequeued in @this.matchSlicesWithSandboxes() and enqueued in fetchTask.\n // We should try to get rid of @this.slices, because it has low utility.\n // However, to minimize churn, this can be done later.\n //\n\n const availableSandboxes = this.unallocatedSpace;\n if (availableSandboxes <= 0) return;\n\n debugging() && console.log(`distributeQueuedSlices: unallocatedSpace ${availableSandboxes}, readied ${JSON.stringify(this.readiedSandboxes.map(s => s.id))}, queuedSlices ${this.queuedSlices.length}`);\n\n const sandboxSlices = await this.matchSlicesWithSandboxes(availableSandboxes);\n\n for (let sandboxSlice of sandboxSlices) {\n\n const { sandbox, slice } = sandboxSlice;\n if (sandbox) {\n debugging() && console.log(`${shortTime()}: distributeQueuedSlices: matched slice ${slice.sliceNumber} with sandbox ${sandbox.id} for job ${slice.jobAddress}, total sandbox count: ${this.sandboxes.length}`);\n if (sandbox.isReadyForAssign) {\n try {\n let timeoutMs = Math.floor(Math.min(+Supervisor.lastAssignFailTimerMs || 0, 10 * 60 * 1000 /* 10m */));\n await asleepMs(timeoutMs);\n if (sandbox.isReadyForAssign) { // Don't need this double-check anymore because of @this.matchSlicesWithSandboxes().\n sandbox.setIsAssigning(); // Don't need this either --> Set the state to assigning before the await to circumvent the event loop problem from await.\n await this.assignJobToSandbox(sandbox, slice.jobAddress);\n } else {\n return // Don't need this either --> The sandbox was picked a second time while it was being prepared, return without an error\n }\n } catch (e) {\n console.error(`Could not assign slice ${slice.sliceNumber} for job ${slice.jobAddress} to sandbox ${sandbox.id}`);\n if (Supervisor.debugBuild) console.error(`...exception`, e);\n Supervisor.lastAssignFailTimerMs = Supervisor.lastAssignFailTimerMs ? +Supervisor.lastAssignFailTimerMs * 1.25 : Math.random() * 200;\n this.returnSlice(slice);\n continue;\n }\n }\n\n if (!Supervisor.lastAssignFailTimerMs)\n Supervisor.lastAssignFailTimerMs = Math.random() * 200;\n this.startSandboxWork(sandbox, slice);\n Supervisor.lastAssignFailTimerMs = false;\n\n } else {\n // We should never get here.\n console.error(\"Supervisor.distributeQueuedSlices: Failed to find sandbox for slice\", {\n jobAddress: slice.jobAddress,\n sliceNumber: slice.sliceNumber\n });\n }\n }\n }\n\n /**\n *\n * @param {Sandbox} sandbox\n * @param {opaqueId} jobAddress\n * @returns {Promise<void>}\n */\n async assignJobToSandbox(sandbox, jobAddress) {\n var ceci = this;\n\n try {\n return sandbox.assign(jobAddress); // Returns Promise.\n } catch(error) {\n // return slice to scheduler, log error\n console.error('Supervisor.assignJobToSandbox: Failed to assign job to sandbox.', {\n jobAddress: jobAddress.substr(0,10),\n error,\n });\n\n ceci.returnSandbox(sandbox);\n\n throw error;\n }\n }\n \n /**\n * Gives a slice to a sandbox which begins working. Handles collecting\n * the slice result (complete/fail) from the sandbox and submitting the result to the scheduler.\n * It will also return the sandbox to @this.returnSandbox when completed so the sandbox can be re-assigned.\n *\n * @param {Sandbox} sandbox - the sandbox to give the slice\n * @param {Slice} slice - the slice to distribute\n * @returns {Promise<void>} Promise returned from sandbox.run\n */\n async startSandboxWork (sandbox, slice) {\n var startDelayMs;\n\n try {\n slice.markAsWorking();\n } catch (e) {\n // This will occur when the same slice is distributed twice.\n // It is normal because two sandboxes could finish at the same time and be assigned the\n // same slice before the slice is marked as working.\n debugging() && console.debug(e);\n return;\n }\n\n // sandbox.requiresGPU = slice.requiresGPU;\n // if (sandbox.requiresGPU) {\n // this.GPUsAssigned++;\n // }\n\n if (Supervisor.startSandboxWork_beenCalled)\n startDelayMs = 1000 * (tuning.minSandboxStartDelay + (Math.random() * (tuning.maxSandboxStartDelay - tuning.minSandboxStartDelay)));\n else {\n startDelayMs = 1000 * tuning.minSandboxStartDelay;\n Supervisor.startSandboxWork_beenCalled = true;\n }\n \n try {\n debugging() && console.log(`${shortTime()} startSandboxWork: started sandbox.id.sliceNumber ${sandbox.id}-${slice.sliceNumber}.${slice.jobAddress}, total sandbox count: ${this.sandboxes.length}`);\n let result = await sandbox.work(slice, startDelayMs);\n slice.collectResult(result, true);\n // In watchdog, all sandboxes in working state, have their status sent to result submitter.\n // However, this can happen after the sandbox/slice has already sent results\n // to result submitter, in which case, the activeSlices table has already removed the row\n // corresponding to slice and hence is incapable of updating status.\n sandbox.changeWorkingToAssigned();\n sandbox.allocated = false; // This is a little redundant: don't await any promises between here and the finally clause.\n this.assignedSandboxes.push(sandbox);\n debugging() && console.log(`${shortTime()} startSandboxWork: completed sandbox.id.sliceNumber ${sandbox.id}-${slice.sliceNumber}.${slice.jobAddress}, total sandbox count: ${this.sandboxes.length}`);\n } catch(error) {\n let logLevel;\n if (error instanceof SandboxError) {\n logLevel = 'warn';\n // The message and stack properties of error objects are not enumerable,\n // so they have to be copied into a plain object this way\n const errorResult = Object.getOwnPropertyNames(error).reduce((o, p) => {\n o[p] = error[p]; return o;\n }, { message: 'Unexpected worker error' });\n slice.collectResult(errorResult, false);\n } else {\n logLevel = 'error';\n // This error was unrelated to the work being done,\n // so just return the slice in the finally block.\n }\n\n // sandbox.public.name is defined in Sandbox.assign.\n // It is possible to get here with !sandbox.public.\n const jobName = sandbox.public ? sandbox.public.name : 'unnamed';\n const errorObject = {\n jobAddress: slice.jobAddress.substr(0,10),\n sliceNumber: slice.sliceNumber,\n sandbox: sandbox.id,\n jobName: jobName,\n };\n \n // 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 && error.errorCode === 'EUNCAUGHTERROR') {\n console[logLevel](`Supervisor.startSandboxWork - Uncaught error in sandbox, could not compute.\\n`, errorObject);\n } else if (!displayMaxInfo && error.errorCode === 'EFETCH') {\n console[logLevel](`Supervisor.startSandboxWork - Could not fetch data: ${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 sandbox.allocated = false;\n\n if (slice.result) {\n await this.recordResult(slice);\n } else {\n this.returnSlice(slice);\n }\n this.returnSandbox(sandbox, slice);\n }\n }\n\n /**\n * Terminates sandboxes and returns slices.\n * Sets the working flag to false, call @this.work to start working again.\n * \n * If forceTerminate is true: Terminates all sandboxes and returns all slices.\n * If forceTerminate is false: Terminates non-working sandboxes and returns queued slices.\n *\n * @param {boolean} [forceTerminate = true] - true if you want to stop the sandboxes from completing their current slice.\n * @returns {Promise<void>}\n */\n async stopWork (forceTerminate = true) {\n if (forceTerminate) {\n let sandbox;\n while (sandbox = this.sandboxes.pop()) {\n sandbox.terminate(false);\n }\n\n await this.returnSlices(this.slices).then(() => {\n this.slices.length = 0;\n this.queuedSlices.length = 0;\n });\n\n this.resultSubmitterConnection.off('close', this.openResultSubmitterConn);\n this.resultSubmitterConnection.close();\n this.resultSubmitterConnection = null;\n } else {\n // Only terminate idle sandboxes and return only queued slices\n let idleSandboxes = this.sandboxes.filter(w => !w.isWorking);\n for (let sandbox of idleSandboxes) {\n sandbox.terminate(false);\n }\n \n await this.returnSlices(this.slices).then(() => {\n this.slices.length = 0;\n this.queuedSlices.length = 0;\n });\n\n await new Promise((resolve, reject) => {\n let sandboxesRemaining = this.workingSandboxes.length;\n if (sandboxesRemaining === 0)\n {\n resolve();\n }\n // Resolve and finish work once all sandboxes have finished submitting their results.\n this.on('submitFinished', () => {\n sandboxesRemaining--;\n if (sandboxesRemaining === 0)\n {\n console.log('All sandboxes empty, stopping worker and closing all connections');\n resolve();\n }\n });\n })\n this.resultSubmitterConnection.off('close', this.openResultSubmitterConn);\n this.resultSubmitterConnection.close();\n this.resultSubmitterConnection = null;\n }\n\n this.taskDistributorConnection.off('close', this.openTaskDistributorConn);\n this.taskDistributorConnection.close();\n this.taskDistributorConnection = null;\n\n this.packageManagerConnection.off('close', this.openPackageManagerConn);\n this.packageManagerConnection.close();\n this.packageManagerConnection = null;\n\n this.eventRouterConnection.off('close', this.openEventRouterConn);\n this.eventRouterConnection.close();\n this.eventRouterConnection = null;\n\n this.emit('stop');\n }\n\n /**\n * Takes a slice and returns it to the scheduler to be redistributed.\n * Usually called when the supervisor told it to forcibly stop working.\n *\n * @param {Slice} slice - the slice to return to the scheduler\n * @returns {Promise<*>} - response from the scheduler the slice was returned to\n */\n returnSlice (slice) {\n\n const jobAddress = slice.jobAddress;\n const sliceNumber = slice.sliceNumber;\n \n const authorizationMessage = this.findAuthorizationMessage('Supervisor.returnSlice:', jobAddress, sliceNumber);\n \n // Remove the slice from the slices array.\n this.removeElement(this.slices, slice);\n\n let payload = slice.getReturnMessagePayload(this.workerOpaqueId, authorizationMessage,\n slice.error ? 'uncaught' : 'unknown');\n \n return this.resultSubmitterConnection.send('status', payload)\n .then(response => {\n return response;\n })\n .catch(error => {\n console.error('Failed to return slice', {\n sliceIdentifier: slice.identifier,\n sliceNumber: sliceNumber,\n jobAddress: jobAddress,\n error,\n });\n })\n }\n\n /** Bulk-return multiple slices, possibly for assorted jobs\n */\n returnSlices(slices) {\n const slicePayload = [];\n\n slices.forEach(slice => {\n addToSlicePayload(slicePayload, slice.jobAddress, slice.sliceNumber, 'return', this.findAuthorizationMessage('Supervisor.returnSlices:', slice.jobAddress, slice.sliceNumber));\n });\n\n return this.resultSubmitterConnection.send('status', {\n worker: this.workerOpaqueId,\n slices: slicePayload,\n });\n }\n\n /**\n * Submits the slice results to the scheduler, either to the\n * work submit or fail endpoints based on the slice status.\n * Then remove the slice from the @this.slices cache.\n *\n * @param {Slice} slice - the slice to submit\n * @returns {Promise<void>}\n */\n async recordResult (slice) {\n debugging('supervisor') && console.log('supervisor: recording result');\n\n // It is possible for slice.result to be undefined when there are upstream errors.\n if ( !(slice && slice.result))\n throw new Error(`recordResult: slice.result is undefined for sliceNumber ${slice.sliceNumber}, jobAdress ${slice.jobAddress}. This is ok when there are upstream errors.`);\n\n const jobAddress = slice.jobAddress;\n const sliceNumber = slice.sliceNumber;\n const authorizationMessage = this.findAuthorizationMessage('Supervisor.recordResult:', jobAddress, sliceNumber);\n\n /* @see result-submitter::result for full message details */\n const metrics = { GPUTime: 0, CPUTime: 0, CPUDensity: 0, GPUDensity: 0 };\n var payloadData = {\n slice: sliceNumber,\n job: jobAddress,\n worker: this.workerOpaqueId,\n paymentAddress: this.paymentAddress,\n metrics,\n authorizationMessage,\n }\n const timeReport = slice.result.timeReport;\n if (timeReport && timeReport.total > 0) {\n metrics.GPUTime = timeReport.webGL;\n metrics.CPUTime = timeReport.CPU;\n metrics.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() && console.log('Supervisor.recordResult: payloadData.result', payloadData.result.slice(0, 512));\n\n try {\n if (slice.completed) {\n\n /* work function returned a result */\n const { success, payload } = await this.resultSubmitterConnection.send(\n 'result',\n payloadData,\n );\n\n if (!success) {\n throw new DCPError('Unable to submit result for work done', payload);\n }\n\n const receipt = {\n accepted: true,\n payment: payload.slicePaymentAmount,\n };\n this.emit('submittedResult', payload);\n this.emit('dccCredit', receipt);\n } else {\n /* slice did not complete for some reason */\n let statusPayloadData = {\n worker: this.workerOpaqueId,\n slices: [\n {\n sliceNumber: sliceNumber,\n job: jobAddress,\n status: 'return', // special state looked for in status.js / result-submitter\n reason: 'uncaught', // special state looked for in status.js / result-submitter\n error: slice.error,\n authorizationMessage,\n }\n ], \n };\n \n await this.resultSubmitterConnection.send('status', statusPayloadData);\n }\n } catch(error) {\n console.info(`1014: Failed to submit results for slice ${payloadData.slice} of job ${payloadData.job}`, error);\n this.emit('submitSliceFailed', error);\n }\n finally\n {\n this.emit('submitFinished');\n // Remove the slice from the slices array\n this.removeElement(this.slices, slice);\n }\n }\n\n /**\n * Send a work function's result to a server that speaks our DCP Remote Data Server protocol.\n * The data server dcp-rds is been implemented in https://gitlab.com/Distributed-Compute-Protocol/dcp-slice-service .\n *\n * @param {Slice} slice - Slice object whose result we are sending.\n * @returns {Promise<object>} - Object of the form { success: true, href: 'http://127.0.0.1:3521/methods/download/jobs/34/result/10' } .\n * @throws When HTTP status not in the 2xx range.\n */\n async sendResultToRemote(slice) {\n const postParams = {\n ...slice.resultStorageParams\n };\n\n const sliceResultUri = makeDataURI('pattern', slice.resultStorageDetails, {\n slice: slice.sliceNumber,\n job: slice.jobAddress,\n });\n\n debugging() && console.log('sendResultToRemote sliceResultUri: ', sliceResultUri);\n\n const url = new DcpURL(sliceResultUri);\n\n // Note: sendResultToRemote was made a member function of class Supervisor to enable access to this.alowedOrigins .\n if (this.allowedOrigins.indexOf(url.origin) === -1 &&\n dcpConfig.worker.allowOrigins.sendResults.indexOf(url.origin) === -1) {\n throw new Error(`Invalid origin for remote result storage: '${url.origin}'`);\n }\n\n postParams.element = slice.sliceNumber;\n postParams.contentType = 'application/json'; // Currently data will be outputed as a JSON object, @todo: Support file upload.\n\n debugging() && console.log('sendResultToRemote: postParams: ', postParams);\n\n let result = slice.result.result;\n if (result) {\n postParams.content = JSON.stringify(result);\n } else {\n postParams.error = JSON.stringify(slice.error);\n }\n\n debugging() && console.log('sendResultToRemote: content: ', (result ? postParams.content : postParams.error).slice(0, 512));\n\n //\n // Notes:\n // 1) In recordResults the response from justFetch is JSON serialized and encodeDataURI is called.\n // payloadData.result = await this.sendResultToRemote(slice);\n // payloadData.result = encodeDataURI(JSON.stringify(payloadData.result));\n // 2) We do further processing after the call to sendResultToRemote in recordResult, because\n // if we did it here there would be a perf hit. When the return value is a promise, it gets\n // folded into sendResultToRemote's main promise. If justFetch's promise wasn't a return value then\n // justFetch would be separately added to the micro-task-queue.\n return await justFetch(url, 'JSON', 'POST', false, postParams);\n }\n}\n\n/**\n * 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{\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 = __webpack_require__(/*! ./slice */ \"./src/dcp-client/worker/slice.js\").SLICE_STATUS_UNASSIGNED; /* ToT */\n console.warn(`Sandbox ${sandbox.id} (${sandbox.public.name}/${slice.sandboxErrorCount}) had error`, error);\n\n if (slice.sandboxErrorCount < dcpConfig.worker.maxSandboxErrorsPerSlice)\n supervisor.queuedSlices.push(slice);\n else\n supervisor.returnSlice(slice);\n}\n\n/**\n * Add a slice to the slice payload being built. If a sliceList already exists for the \n * job-status-authMessage tuple, then the slice will be added to that, otherwise a new \n * sliceList will be added to the payload.\n *\n * @param {Object[]} slicePayload Slice payload being built. Will be mutated in place.\n * @param {Address} job Address of job the slice belongs to\n * @param {Number} sliceNumber Slice number\n * @param {String} status Status update, eg. progress or scheduled\n * @param {Object} authorizationMessage authorizationMessage for the slice\n *\n * @returns {Object[]} mutated slicePayload array\n */\nfunction addToSlicePayload(slicePayload, job, sliceNumber, status, authorizationMessage) {\n // Try to find a sliceList in the payload which matches the job, status, and auth message\n let sliceList = slicePayload.find(desc => {\n return desc.job === job && desc.status === status && desc.authorizationMessage === authorizationMessage;\n });\n\n // If we didn't find a sliceList, start a new one and add it to the payload\n if (!sliceList) {\n sliceList = {\n job,\n sliceNumbers: [],\n status,\n authorizationMessage: authorizationMessage,\n };\n slicePayload.push(sliceList);\n }\n\n sliceList.sliceNumbers.push(sliceNumber);\n\n return slicePayload;\n}\n\n/**\n * Return DCPv4-specific connection options, composed of type-specific, URL-specific, \n * and worker-specific options, any/all of which can override the dcpConfig.dcp.connectOptions.\n * The order of precedence is the order of specificity.\n */\nfunction connectionOptions(url, label) {\n return leafMerge(/* ordered from most to least specific */\n dcpConfig.worker.dcp.connectionOptions.default,\n dcpConfig.worker.dcp.connectionOptions[label],\n dcpConfig.worker.dcp.connectionOptions[url.href]);\n}\n\n/** @type {number | boolean} */\nSupervisor.lastAssignFailTimerMs = false;\n/** @type {boolean} */\nSupervisor.startSandboxWork_beenCalled = false;\n/** @type {boolean} */\nSupervisor.debugBuild = (__webpack_require__(/*! dcp/common/dcp-build */ \"./src/common/dcp-build.js\").build === 'debug');\n\nexports.Supervisor = Supervisor;\n\n\n//# sourceURL=webpack:///./src/dcp-client/worker/supervisor.js?");
|
|
5390
5390
|
|
|
5391
5391
|
/***/ }),
|
|
5392
5392
|
|
|
@@ -5445,7 +5445,7 @@ eval("/**\n * @file protocol/connection/batch.js\n * @author Ryan Ro
|
|
|
5445
5445
|
/***/ (function(module, exports, __webpack_require__) {
|
|
5446
5446
|
|
|
5447
5447
|
"use strict";
|
|
5448
|
-
eval("/**\n * @file protocol/connection/connection.js\n * @author Ryan Rossiter\n * @author KC Erb\n * @date January 2020, Feb 2021\n *\n * A Connection object represents a connection to another DCP entity. \n * A DCP connection may 'live' longer than the underlying protocol's connection,\n * and the underlying protocol connection (or, indeed, protocol) may change\n * throughout the life of the DCP connection.\n * \n * DCP connections are uniquely identified by the DCP Session ID, specified by\n * the dcpsid property, present in every message body. This session id negotiated during connection,\n * with the initiator and target each providing half of the string.\n *\n * \n * State Transition Diagram for Connection.state:\n *\n * initial connecting established waiting close-wait closing closed\n * ====================================================================================================\n * |- connect() ->\n * |-----------------------------------------------------------------------------------> doClose()\n * |----- connect() ----->\n * |- establishTarget() ->\n * <-- revive() --|\n * |--------- doClose() ------->\n * |- doClose() ->\n * |--------------|---------------------|-------------> failTransport()\n * |--------------|---------------------|--------------|------------|------------> <------------| doClose()\n *\n * failTransport() takes a state from anywhere, sets it to waiting,\n * and sends it back to where it came from. doclose() takes a state\n * from anywhere and sends it to the coClose() state.\n *\n * Not until the established state can we count on things like a dcpsid, \n * peerAddress, identityPromise resolution and so on.\n */\n\n\n\nconst debugging = __webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope('dcp');\nconst { EventEmitter } = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\");\nconst wallet = __webpack_require__(/*! dcp/dcp-client/wallet */ \"./src/dcp-client/wallet/index.js\");\nconst { DcpURL } = __webpack_require__(/*! dcp/common/dcp-url */ \"./src/common/dcp-url.js\");\nconst { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\nconst { assert } = __webpack_require__(/*! dcp/common/dcp-assert */ \"./src/common/dcp-assert.js\");\nconst { leafMerge, stringify } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\nconst kvin = __webpack_require__(/*! kvin */ \"./node_modules/kvin/kvin.js\");\nconst { Synchronizer } = __webpack_require__(/*! dcp/common/concurrency */ \"./src/common/concurrency.js\");\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\n\nconst perf = typeof performance === 'undefined'\n ? requireNative('perf_hooks').performance\n : performance;\n\nconst { Transport } = __webpack_require__(/*! ../transport */ \"./src/protocol-v4/transport/index.js\");\nconst { Sender } = __webpack_require__(/*! ./sender */ \"./src/protocol-v4/connection/sender.js\");\nconst { Receiver } = __webpack_require__(/*! ./receiver */ \"./src/protocol-v4/connection/receiver.js\");\nconst { MessageFactory } = __webpack_require__(/*! ./message-factory */ \"./src/protocol-v4/connection/message-factory.js\");\nconst { MessageLedger } = __webpack_require__(/*! ./message-ledger */ \"./src/protocol-v4/connection/message-ledger.js\");\nconst { ValidityStampCache } = __webpack_require__(/*! ./validity-stamp-cache */ \"./src/protocol-v4/connection/validity-stamp-cache.js\");\nconst { getGlobalIdentityCache } = __webpack_require__(/*! ./identity-cache */ \"./src/protocol-v4/connection/identity-cache.js\");\nconst { makeEBOIterator, setImmediateN } = __webpack_require__(/*! dcp/common/dcp-timers */ \"./src/common/dcp-timers.js\");\n\nconst { ConnectionMessage } = __webpack_require__(/*! ./message */ \"./src/protocol-v4/connection/message.js\");\nconst { ConnectionRequest } = __webpack_require__(/*! ./request */ \"./src/protocol-v4/connection/request.js\");\nconst { ConnectionResponse } = __webpack_require__(/*! ./response */ \"./src/protocol-v4/connection/response.js\");\nconst { ConnectionBatch } = __webpack_require__(/*! ./batch */ \"./src/protocol-v4/connection/batch.js\");\nconst { ConnectionAck } = __webpack_require__(/*! ./ack */ \"./src/protocol-v4/connection/ack.js\");\nconst { ErrorPayloadCtorFactory } = __webpack_require__(/*! ./error-payload */ \"./src/protocol-v4/connection/error-payload.js\");\nconst isDebugBuild = __webpack_require__(/*! dcp/common/dcp-build */ \"./src/common/dcp-build.js\").build === 'debug';\n\nlet globalConnectionId = 0;\n\nconst CONNECTION_STATES = [\n 'initial',\n 'connecting',\n 'established',\n 'waiting', /* Targets only, when send fails */\n 'close-wait', /* Target of close message is in this state until response is acknowledged */\n 'closing',\n 'closed',\n]\n\nclass Connection extends EventEmitter {\n static get VERSION() {\n return '5.0.0'; // Semver format\n }\n\n static get VERSION_COMPATIBILITY() {\n return '5.0.0'; // Semver format, can be a range\n }\n\n /**\n * @constructor Connection form 4:\n * Create a DCP Connection object for an initiator.\n * @param {string} target The string version (ie href) of the URL of the target to connect to.\n * @param {wallet.IdKeystore} [idKeystore]\n * @param {Object} [connectionOptions]\n * @see form 1\n */\n /**\n * @constructor Connection form 3:\n * Create a DCP Connection object for an initiator.\n * @param {DcpURL|URL} target The URL of the target to connect to.\n * @param {wallet.IdKeystore} [idKeystore]\n * @param {Object} [connectionOptions]\n * @see form 1\n */\n /**\n * @constructor Connection form 2:\n * Create a DCP Connection object for a target.\n * @param {wallet.IdKeystore} [idKeystore]\n * @param {Object} [connectionOptions]\n * @see form 1\n */\n /**\n * @constructor Connection form 1\n * Create a DCP Connection object. \n * \n * @note Connection objects exist for the lifetime of a given DCP connection \n * (session), whether or not the underlying transport (eg internet protocol) is connected or not. Once \n * the DCP session has ended, this object has no purpose and is not reusable.\n * \n * @param {object|undefined} target Undefined when we are the target, or an object describing the target. This object \n * may contain the following properties; 'location' is mandatory:\n * - location: a DcpURL that is valid from the Internet\n * - friendLocation: a DcpURL that is valid from an intranet; if\n * both location and friendLocation specified, the best\n * one will be chosen by examining IP addresses\n * - identity: a object with an address property which is an\n * instanceof wallet.Address which corresponds to the peer's\n * identity; this overrides the identity cache unless\n * connectionOptions.strict is truey.\n * @param {wallet.IdKeystore} [idKeystore] The keystore used to sign messages; used for non-repudiation.\n * If not specified, a dynamically-generated keystore will be used.\n * \n * @param {Object} [connectionOptions] Extra connection options that aren't defined via dcpConfig.dcp.connectionOptions.\n * These options include:\n * - identityUnlockTimeout: Number of (floating-point) seconds to leave the identity \n * keystore unlocked between invocations of Connection.send\n */\n constructor(target, idKeystore, connectionOptions)\n {\n /* polymorphism strategy: rewrite all to form 1 before super */\n if (target instanceof wallet.Keystore) /* form 2 */\n { \n connectionOptions = arguments[2];\n idKeystore = arguments[1];\n target = undefined;\n }\n if (typeof connectionOptions === 'undefined')\n connectionOptions = {};\n\n if (target instanceof URL) /* form 3.2 */\n target = { location: new DcpURL(target) };\n else if (DcpURL.isURL(target)) /* form 3.1 */\n target = { location: new DcpURL(target) };\n else if (target instanceof String || typeof target === 'string') /* form 4 */\n target = { location: new DcpURL(target) };\n\n assert((typeof target === 'undefined') || (typeof target === 'object' && DcpURL.isURL(target.location)));\n assert(typeof connectionOptions === 'object');\n \n super(`Protocol Connection (${target ? 'initiator' : 'target'})`);\n\n if (target) {\n this.isInitiator = true;\n this.debugLabel = 'connection(i):';\n this._target = target;\n } else {\n this.isInitiator = false;\n this.debugLabel = 'connection(t):';\n }\n\n if (idKeystore) {\n this.identityPromise = (async () => await idKeystore)();\n } else {\n this.identityPromise = wallet.getId();\n }\n\n this.identityPromise.then((keystore) => {\n this.identity = keystore;\n debugging('connection') && console.debug(this.debugLabel, 'identity is', keystore.address);\n return keystore;\n });\n\n this.ttl = {\n ntp: false,\n }\n \n // Init internal state / vars\n this.state = new Synchronizer(CONNECTION_STATES[0], CONNECTION_STATES);\n this.state.on('change', (s) => this.emit('readyStateChange', s) );\n\n this._id = globalConnectionId++;\n this.debugLabel = this.debugLabel.replace(')', `#${this._id})`);\n debugging('connection') && console.debug(this.debugLabel, 'connection id is', this._id, `target is ${target && target.location}`);\n this.dcpsid = null;\n this.peerAddress = null;\n this.transport = null;\n this.maxConnectionTimeout = null;\n this.messageFactory = new MessageFactory(this);\n this.messageLedger = new MessageLedger(this);\n this.authorizedSender = null;\n \n this.Message = ConnectionMessage(this);\n this.Request = ConnectionRequest(this.Message);\n this.Response = ConnectionResponse(this.Message);\n this.Batch = ConnectionBatch(this.Message);\n this.Ack = ConnectionAck(this.Message);\n this.ErrorPayload = ErrorPayloadCtorFactory(this);\n this.connectTime = Date.now();\n\n this.openRequests = {};\n ValidityStampCache.init();\n\n this.receiver = new Receiver(this, this.messageLedger);\n this.receiver.on('request', (req) => this.emit('request', req));\n\n debugging('connection') && console.debug(this.debugLabel, `new; ${target && target.location || '<target>'}`);\n\n this.configureConnectionForUrl = (url) => {\n if (typeof connectionOptions.ttl === 'number')\n connectionOptions = Object.assign({}, connectionOptions, { ttl: { default: connectionOptions.ttl } });\n \n this.url = url;\n this.connectionOptions = leafMerge(/* ordered from least to most specific */\n dcpConfig.dcp.connectionOptions.default,\n this.url && dcpConfig.dcp.connectionOptions[this.url.hostname],\n this.url && dcpConfig.dcp.connectionOptions[this.url.origin],\n dcpConfig.dcp.connectionOptions[this.isInitiator ? this.url.href : 'target'],\n connectionOptions\n );\n\n // ensure we're using a _copy_ of the transports list, not mutating the original\n this.connectionOptions.transports = [...this.connectionOptions.transports];\n this.originalConnectionTransports = [].concat(this.connectionOptions.transports);\n this.transportsTried = 0;\n\n this.ttl = leafMerge(this.ttl, this.connectionOptions.ttl);\n this.unlockTimeout = this.connectionOptions.identityUnlockTimeout;\n this.connectionOptions.id = this._id;\n this.EBOIterator = makeEBOIterator(1000, 60000);\n\n assert(this.unlockTimeout >= 0);\n assert(typeof this.connectionOptions.ttl.min === 'number');\n assert(typeof this.connectionOptions.ttl.max === 'number');\n assert(typeof this.connectionOptions.ttl.default === 'number');\n\n this.secureLocation = determineIfSecureLocation(this);\n }\n }\n\n /**\n * This method is an instantiator/factory function for building a connection\n * that will act as the target in a new protocol connection. It's a little\n * like making a new connection and calling `connect` on it, except that\n * instead of having a url to connect to we have a transport which should\n * be ready to emit the connect message from the initiator.\n * \n * @param {wallet.Keystore} ks - Keystore to associate to the new connection.\n */\n static async newTarget(url, ks, transport) {\n const target = new Connection(undefined, ks, {\n ttl: {\n ntp: true, // targets should always be NTP-synced\n }\n });\n\n target.configureConnectionForUrl(url);\n assert(target.connectionOptions.ttl.ntp);\n \n await target.identityPromise;\n target.transport = transport;\n target.sender = new Sender(target);\n target.state.set('initial', 'connecting');\n return target;\n }\n\n /**\n * When invoked by the initator, this method establishes the connection by connecting\n * to the target provided to the constructor.\n * \n * When a dcpConfig-fragment is used as the target object, we determine automatically if \n * we should be using the location or the friendLocation property at this point.\n *\n * This function is also where the connectionOptions are merged together. This means \n * that if either of these objects come from dcpConfig and we HUP a service running this\n * code, that the new configuration will be used to connect, instead of the configuration \n * that was defined at the time of construction.\n *\n * @throws when called by a target, or when called more than once\n */\n async connect() {\n var presharedPeerAddress;\n \n let transportName;\n \n if (!this.state.in(['initial', 'connecting'])) {\n throw new Error(`Can only invoke connect in initial/connecting state. Current state: ${this.state.valueOf()}.`, );\n }\n\n // make connect re-entrant if called during the connecting phase\n if (this.state.is('connecting'))\n {\n /* nth connect attempt on same Connection instance before actual connection -- swallow attempts and wait until connected */\n do\n {\n await this.state.until(['established', 'close-wait', 'closing', 'closed']);\n } while(this.state.is('waiting'));\n\n if (!this.state.is('established'))\n throw new Error(`Could not establish connection to ${this.url}; in state ${this.state}`);\n return;\n }\n\n this.state.set('initial', 'connecting');\n\n if (this._target && this._target.hasOwnProperty('friendLocation') && await isFriendlyURL(this._target.friendLocation))\n this.configureConnectionForUrl(this._target.friendLocation);\n else\n this.configureConnectionForUrl(this._target.location);\n\n const transport = this.fetchTransport();\n this.transport = transport.transport;\n transportName = transport.moduleName;\n this.sender = new Sender(this);\n // create sender before promises so that we can still enqueue messages before hopping off the event loop\n await this.identityPromise;\n await this.connectToTarget(transportName);\n const establishResults = await this.sender.establish().catch( (err) => {\n console.error('Failed to establish connection.');\n this.close(err, true);\n throw err;\n });\n const dcpsid = establishResults.dcpsid;\n const peerAddress = wallet.Address(establishResults.peerAddress);\n\n if (!this.connectionOptions.strict && this._target.identity)\n {\n if (determineIfSecureConfig())\n {\n let identity = await this._target.identity;\n if (!(identity instanceof wallet.Keystore))\n identity = { address: new wallet.Address(identity) }; /* map strings and Addresses to ks ducks */\n\n presharedPeerAddress = identity.address;\n debugging('connection') && console.debug('Using preshared peer address', presharedPeerAddress);\n }\n }\n this.ensureIdentity(peerAddress, presharedPeerAddress); /** XXXwg possible resource leak: need cleanup; need try {} catch->emit(cleanup) */\n \n // checks have passed, now we can set props\n this.peerAddress = peerAddress;\n if (this.dcpsid)\n throw new DCPError(`Reached impossible state in connection.js; dcpsid already specified ${this.dcpsid} (${this.url})`, 'DCPC-1004');\n this.dcpsid = dcpsid;\n\n // Update state\n this.state.set('connecting', 'established'); /* established => dcpsid has been set */\n this.emit('connected', this.url);\n this.sender.finishSetup();\n }\n\n moreTransportsExist() {\n return this.connectionOptions\n && this.connectionOptions.transports\n && Array.isArray(this.connectionOptions.transports)\n && this.connectionOptions.transports.length > 0;\n }\n \n /**\n * Initiators only.\n * Try to get and establish a transport from the transports list.\n * @returns {Transport} a new transport instance or false if no transports are left to try\n */\n fetchTransport() {\n //We want to cycle our possible transports until the maxConnectionTimeout runs out or we have a connection\n const moduleName = this.connectionOptions.transports.shift();\n this.connectionOptions.transports.push(moduleName);\n this.transportsTried++;\n debugging() && console.log(this.debugLabel, 'fetchTransport trying ', moduleName, `remaining transports: ${this.connectionOptions.transports.length}`)\n if (!moduleName) {\n console.error('Error: All transports have failed to connect/transmit within limits. Closing connection.');\n this.close('all transports failed', true).catch(error => {\n console.error(`Error: failed to close transport:`, error);\n });\n throw new DCPError(`Failed to connect to ${this.url}, no transport could be established`, 'ENOTRANSPORT');\n }\n const TransportClass = Transport.require(moduleName);\n const transport = new TransportClass(null, this.connectionOptions);\n transport.on('message', (m) => {\n this.onMessage(JSON.parse(m))\n });\n return { transport, moduleName };\n }\n\n /**\n * Initiators only\n * Resolves when we have connected to target using a transport.\n * Calls \"failTransport\" if we run out of time / attempts to connect.\n * Let's transport handle failure but also is watching for it too.\n */\n async connectToTarget(currentTransport) {\n debugging('connection') && console.log(this.debugLabel, `Using transport \"${this.transport.name}\" ... connecting to ${this.url}.`);\n const globalWaitMs = dcpConfig.dcp.maxConnectionTimeout;\n const connectWaitMs = this.transport.connect(this.url, this.connectionOptions);\n\n this.maxConnectionTimeout = this.maxConnectionTimeout ? this.maxConnectionTimeout\n : setTimeout(this.close.bind(this, `Timeout to have a transport connect reached at ${globalWaitMs} ms`, true), globalWaitMs);\n\n await new Promise( (resolve) => {\n // Listener for a connect-failed, will invoke EBO algorithm to retry the connection\n this.transport.once('connect-failed', async () => {\n if (this.transportsTried >= this.connectionOptions.transports.length)\n {\n this.transportsTried = 0;\n const backoffTime = this.EBOIterator.next().value;\n debugging('connection') && console.debug(this.debugLabel, 'connecting in', backoffTime / 1000, 'seconds');\n await this.failTransport('Connect-failed event handler fired.', backoffTime);\n }\n else\n {\n await this.failTransport('Connect-failed event handler fired.');\n }\n resolve();\n });\n\n this.transport.once('connected', () => {\n clearTimeout(this.maxConnectionTimeout);\n this.maxConnectionTimeout = null;\n /* When we successfully connect a transport, we know that transport has potential to consistently work,\n * and disconnects may happen for unrelated reason (such as switching wifi causing a disconnect). \n * Thus, we recreate our transport options list with this transport at the top\n */\n this.connectionOptions.transport = [currentTransport].concat(this.originalConnectionTransports);\n resolve();\n });\n });\n this.transport.on('reconnect', () => {\n debugging('connection') && console.log(this.debugLabel, `Transport reconnected to ${this.url}`);\n /**\n * This may not work at the moment.\n * We don't want to naively send a keepalive because it might go to a closing/closed connection and throw an exception.\n * DCP-2528 is the bug for removing the keepalive in the reconnect handler until we can do it right.\n * DCP-2527 is the bug for making the reconnect handler work properly.\n */\n this.emit('reconnect');\n });\n }\n\n /**\n * Target gains full status once dcpsid and peerAddress\n * are provided by first connect request.\n * @param {string} dcpsid dcpsid\n * @param {wallet.Address} peerAddress Address of peer\n */\n establishTarget(dcpsid, peerAddress) {\n this.connectResponseId = Symbol(); // un-register ConnectResponse\n this.peerAddress = peerAddress;\n if (this.dcpsid)\n throw new DCPError(`Reached impossible state in connection.js; dcpsid already specified ${this.dcpsid}!=${dcpsid} (${this.url})`, 'DCPC-1005');\n this.dcpsid = dcpsid;\n this.state.set('connecting', 'established'); /* established => dcpsid has been set */\n }\n\n ensureIdentity (peerAddress, presharedPeerAddress)\n {\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}`, 'EADDRCHANGE');\n }\n\n /**\n * Check that the transport has given us a message worth dealing with then\n * either let the receiver handle it (message) or the message ledger (ack).\n * @param {object} message parsed (but not validated) message object\n */\n async onMessage (message) {\n var validation;\n var messageError;\n var messageValid = true;\n \n if (this.state.is('closed')) {\n debugging('connection') && console.warn(this.debugLabel, 'onMessage was called on a closed connection.');\n return;\n }\n /**\n * We always ack a duplicate transmission.\n * This must happen before validation since during startup we may lack a\n * nonce or dcpsid (depending on whether initiator or target + race).\n */\n if (this.isDuplicateTransmission(message)) {\n debugging('connection') && console.debug(this.debugLabel, 'duplicate message:', message.body);\n this.transport.send(this.lastAckSigned);\n return;\n }\n\n /* Capture the initial identity of the remote end during the connect operation */\n if (this.authorizedSender === null)\n {\n let messageBody = message.body;\n let payload = messageBody.payload;\n \n if (payload && message.body.type === 'batch')\n {\n for (let i=0; i < payload.length; i++)\n {\n let innerMessageBody = payload[i];\n\n if (innerMessageBody.payload && innerMessageBody.payload.operation === 'connect' && (innerMessageBody.type === 'response' || innerMessageBody.type === 'request'))\n {\n messageBody = innerMessageBody;\n payload = innerMessageBody.payload;\n break;\n }\n }\n }\n\n if (payload)\n {\n if (payload.operation === 'connect' && (messageBody.type === 'response' || messageBody.type === 'request'))\n this.authorizedSender = message.owner;\n else\n throw new Error('Message payload received before connection operation');\n }\n }\n else\n {\n if (message.owner !== this.authorizedSender)\n {\n messageError = new DCPError('Message came from invalid sender.', 'DCPC-1002');\n debugging('connection') && console.debug(this.debugLabel, 'Message owner was not an authorized sender - aborting connection');\n this.close(messageError, true);\n this.emit('error', messageError);\n return;\n }\n }\n\n if (!this.isInitiator && this.state.in(['connecting']))\n {\n // while connecting, the target gets its nonce from the initiator\n if (!this.isInitiator) this.sender.nonce = message.body.nonce;\n }\n\n validation = this.validateSignature(message);\n if (validation.success !== true)\n {\n messageError = new DCPError(`invalid message: ${validation.errorMessage}`, 'DCPC-1002');\n debugging('connection') && console.debug(this.debugLabel, 'Message failed validation -', validation.errorMessage);\n this.close(messageError, true);\n messageValid = false;\n }\n\n if (message.body.type === 'unhandled-message')\n {\n /* This special message type may not have a dcpsid, peerAddress, etc., so it might not\n * validate. It is also not a \"real\" message and only used to report ConnectionManager routing \n * errors, so we just report here, drop it, and close the connection.\n *\n * Note also that this is probably the wrong way to handle this case - restarting daemons - but\n * that is a problem for another day. /wg nov 2021\n */\n let messageError = new DCPError(`target could not process message (${message.payload && message.payload.name || 'unknown error'})`,'DCPC-1002');\n debugging('connection') && console.warn(this.debugLabel, \"Target Error - target could not process message.\", JSON.stringify(message.body),\n \"Aborting connection.\");\n this.close(messageError, true);\n messageValid = false;\n }\n\n validation = this.validateMessage(message);\n if (validation.success !== true)\n {\n let messageError = new DCPError(`invalid message: ${validation.errorMessage}`, 'DCPC-1002');\n debugging('connection') && console.debug(this.debugLabel, 'Message failed validation -', validation.errorMessage);\n this.close(messageError, true);\n messageValid = false;;\n }\n\n if (!messageValid) {\n message.body.type = 'unhandled-message'\n this.emit('error', messageError);\n }\n \n if (message.body.type === \"ack\") {\n const ack = new this.Ack(message.body);\n this.messageLedger.handleAck(ack);\n return;\n } else if (message.body.type !== 'unhandled-message') {\n this.lastMessage = message;\n await this.ackMessage(message);\n }\n \n this.receiver.onMessage(message);\n }\n\n async ackMessage(message) {\n debugging('connection') && console.debug(this.debugLabel, 'acking message.');\n const ack = new this.Ack(message);\n const signedMessage = await ack.sign(this.identity);\n this.transport.send(signedMessage);\n this.lastAck = ack;\n this.lastAckSigned = signedMessage;\n }\n\n /**\n * Checks if the batch we just received has the same nonce\n * as the most-recently received batch.\n * @param {object} messageJSON\n */\n isDuplicateTransmission(messageJSON) {\n return this.lastMessage && this.lastMessage.body.nonce === messageJSON.body.nonce;\n }\n\n /**\n * Validate that the signature was generated from this message body\n * @param {Object} message\n * @returns {Object} with properties 'success' and 'errorMessage'. When the message is valid on its \n * face, the success property is true, otherwise it is is false. When it is false,\n * the errorMessage property will be a string explaining why.\n */\n validateSignature(message)\n {\n if (!message.signature) {\n debugging('connection') && console.warn(\"Message does not have signature, aborting connection\");\n return { success: false, errorMessage: \"message is missing signature\" };\n }\n \n const owner = new wallet.Address(message.owner);\n const signatureValid = owner.verifySignature(message.body, message.signature);\n\n if (!signatureValid)\n {\n debugging('connection') && console.warn(\"Message has an invalid signature, aborting connection\");\n return { success: false, errorMessage: \"invalid message signature\" };\n }\n\n return { success: true };\n }\n \n /**\n * This method is used to perform validation on all types of messages.\n * It validates the DCPSID, nonce, and the peerAddress.\n * @param {Object} message\n * @returns {Object} with properties 'success' and 'errorMessage'. When the message is valid on its \n * face, the success property is true, otherwise it is is false. When it is false,\n * the errorMessage property will be a string explaining why.\n *\n */\n validateMessage(message)\n {\n try\n {\n if (this.peerAddress && !this.peerAddress.eq(message.owner))\n {\n debugging('connection') && console.warn(\"Received message's signature address does not match peer address, aborting connection\\n\",\n \"(signature addr)\", message.owner, '\\n',\n \"(peer addr)\", this.peerAddress);\n return { success: false, errorMessage: \"received message signature does not match peer address\" };\n }\n\n if (this.state.in(['established', 'closing', 'close-wait']) && message.body.type !== 'unhandled-message')\n {\n const body = message.body;\n\n assert(this.peerAddress); /* should be set in connect */\n /**\n * Security note:\n * We don't require the dcpsid to match on an ack because the connect response\n * ack doesn't have a dcpsid until after it is processed. Also ack's are protected\n * by ack tokens and signatures, so this doesn't leave a hole, just an inconsistency.\n */\n if (body.type !== 'ack' && body.dcpsid !== this.dcpsid)\n {\n debugging('connection') && console.warn(\"Received message's DCPSID does not match, aborting connection\\n\",\n \"Message owner:\", message.owner, '\\n',\n \"(ours)\", this.dcpsid, (Date.now() - this.connectTime)/1000, \"seconds after connecting - state:\", this.state._, \"\\n\", \n \"(theirs)\", body.dcpsid);\n if(body.dcpsid.substring(0, body.dcpsid.length/2) !== this.dcpsid.substring(0, this.dcpsid.length/2)){\n debugging('connection') && console.warn(\" Left half of both DCPSID is different\");\n }\n if(body.dcpsid.substring(body.dcpsid.length/2 + 1, body.dcpsid.length) !== this.dcpsid.substring(this.dcpsid.length/2 + 1, body.dcpsid.length)){\n debugging('connection') && console.warn(\" Right half of both DCPSID is different\");\n }\n return { success: false, errorMessage: \"DCPSID do not match\" };\n }\n\n if (body.type !== 'ack' && this.lastAck.nonce !== body.nonce)\n {\n debugging('connection') && console.warn(\"Received message's nonce does not match expected nonce, aborting connection\\n\");\n debugging('connection') && console.debug(this.debugLabel, this.lastAck.nonce, body.nonce);\n return { success: false, errorMessage: \"received message's nonce does not match expected nonce\" };\n }\n }\n\n return { success: true };\n }\n catch(error)\n {\n console.error('message validator failure:', error);\n return { success: false, errorMessage: 'validator exception ' + error.message };\n }\n\n return { success: false, errorMessage: 'impossible code reached' };\n }\n\n /**\n * Targets Only.\n * The receiver creates a special connect response and the connection\n * needs to know about it to get ready for the ack. See `isWaitingForAck`.\n * @param {Message} message message we are sending out and waiting to\n * ack'd, probably a batch containing the response.\n */\n registerConnectResponse(message) {\n this.connectResponseId = message.id;\n }\n\n /**\n * Targets only\n * During the connection process a target sends a connect\n * response to an initiator and the initiator will ack it. Since transports\n * are not tightly coupled, we have no authoritative way to route the ack back\n * to the right connection. So a connection briefly registers the ack it\n * is looking for in this case. It will formally validate the ack after routing.\n * @param {string} messageId id of the message this ack is acknowledging.\n */\n isWaitingForAck(messageId) {\n return messageId === this.connectResponseId;\n }\n\n /**\n * Targets: nothing to do, we are waiting for initiator.\n * Initiators: give up on current transport and try next available option.\n * For now we just have socketio so that means we throw an error.\n * @return {Promise<boolean>} indicating whether we were able to find a new transport.\n */\n async failTransport (failureDescription, retryWaitTime = 0) {\n let transportName;\n const preWaitingState = this.state.valueOf(); /* XXXwg bogus - need explicit and reasonable source states */\n\n /* If we are already in process of closing or already waiting, we don't want to beat \n * the dead horse. Let it stay dead with no transport\n */\n if (['closing', 'closed', 'close-wait', 'waiting'].includes(preWaitingState))\n return;\n\n this.state.testAndSet(preWaitingState, 'waiting');\n debugging('connection') && console.log(this.debugLabel, `Transport \"${this.transport.name}\" has failed.`);\n \n if (this.isInitiator) {\n if (!this.moreTransportsExist()) {\n console.error('Transport failed:', failureDescription);\n // fetchTransport will immediately fail\n }\n debugging('connection') && console.log(this.debugLabel, `Attempting to use next transport.`);\n this.transport.close();\n const transport = this.fetchTransport();\n this.transport = transport.transport;\n transportName = transport.moduleName;\n \n await new Promise(r => setTimeout(r, retryWaitTime));\n \n if (['closing', 'closed', 'close-wait'].includes(this.state.valueOf())) // If some other process closes the connection, don't try to reconnect\n return;\n\n this.state.set('waiting', preWaitingState);\n await this.connectToTarget(transportName);\n }\n }\n\n /**\n * Targets only\n * Give target another transport to try sending messages on again.\n */\n revive(transport) {\n this.transport = transport;\n this.state.set('waiting', 'established');\n }\n\n /**\n * Put connection into close-wait state so that a call to `close`\n * in this state will *not* trigger sending a `close` message to the peer.\n * Then call close.\n *\n * @note: This function is called when the remote end of the transport sends\n * a close command\n */\n closeWait (errorCode = null)\n {\n const preCloseState = this.state.valueOf();\n\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 let reason\n if (errorCode) {\n reason = new DCPError(`Received close from peer with Error Code ${errorCode}`, errorCode);\n } else {\n reason = 'Received close from peer';\n }\n if (this.url)\n (reason instanceof Error) ? reason.message += ` (${this.url})` : reason += ` (${this.url})`;\n\n // If we're already closing, wait for it to complete then resolve\n // WARNING: any place we transition to closing or close-wait, we MUST guarantedd\n // that 'end' will be emitted, or this code will hang forever!\n if (this.state.in(['close-wait', 'closing'])) {\n return new Promise((resolve) => {\n this.once('end', resolve) /* eventually fired by doClose elsewhere */\n });\n }\n\n if (this.state.is('closed')) /* closed somehow on us during await */\n return;\n\n this.state.set(preCloseState, 'close-wait');\n return this.doClose(preCloseState, reason, true);\n }\n\n /**\n * This method will begin gracefully closing the protocol connection.\n * It will only close after sending all pending messages.\n * \n * @param {string|Error} [reason] Either an Error or a message to use in the Error that will reject pending sends.\n * @param {boolean} [immediate] If true, does not wait to send messages or the `close` request.\n *\n * @return a Promise which resolves when the connection has been confirmed closed and the end event has been fired.\n */\n close (reason=null, immediate=false)\n {\n if (this.state.is('closed')) return Promise.resolve();\n\n const preCloseState = this.state.valueOf();\n debugging('connection') && \n console.debug(this.debugLabel, \n `close; dcpsid=${this.dcpsid} state=${preCloseState} reason=${reason} immediate=${immediate} stack=${new Error().stack}`);\n\n // If we're already closing, wait for it to complete then resolve\n if (this.state.in(['close-wait', 'closing'])) {\n return new Promise((resolve) => {\n this.once('end', resolve)\n });\n }\n\n // Put in closing state no matter the current state\n this.state.set(preCloseState, 'closing');\n\n // Perform actual work of closing\n return this.doClose(preCloseState, reason, immediate);\n }\n\n /**\n * sends close message to peer and waits for response\n * @note: This function is not reentrant!\n */\n async closeGracefully(reason) {\n if (this.transport)\n {\n /* If we got as far as initializing a transport during connect(), send close\n * message to peer, should get a response before time is up.\n */\n const closeMessage = this.messageFactory.buildMessage('close');\n if (reason instanceof Error)\n closeMessage.payload.errorCode = reason.code;\n this.sender.enqueue(closeMessage)\n await new Promise(r => setImmediateN(r, 30));\n this.messageLedger.fulfillMessagePromise(closeMessage.id, {});\n }\n }\n\n /** sends close message to peer but doesn't require response \n */\n async closeImmediately(reason) {\n if(this.sender.inFlight)\n this.sender.clearFlightDeck(this.sender.inFlight.message, this.sender.nonce);\n \n let closeMessage = this.messageFactory.buildMessage('close');\n if (reason instanceof Error)\n closeMessage.payload.errorCode = reason.code;\n closeMessage.ackToken = this.sender.makeAckToken();\n closeMessage.nonce = this.sender.nonce;\n let signedCloseMessage = await closeMessage.sign();\n \n this.sender.inFlight = { message: closeMessage, signedMessage: signedCloseMessage };\n debugging('connection') && console.debug(this.debugLabel, 'sending close message to peer');\n this.transport.send(signedCloseMessage).catch(error => console.info(this.debugLabel, 'failed to send close message to peer', error.message));\n }\n \n /**\n * Core close functionality shared by `close` and `closeWait`\n *\n * @param {string} preCloseState the state that the connection was in at the start of the\n * invocation of close() or closeWait()\n *\n * @note: this function is not reentrant due to closeGracefully\n */\n async doClose(preCloseState, reason, immediate) {\n const dcpsid = this.dcpsid;\n\n try\n {\n assert(this.state.valueOf() === 'closing' || this.state.valueOf() === 'close-wait');\n \n // Emit the close event the moment we know we are going to close, \n // so we can catch the close event and reopen the connection\n this.emit('close', dcpsid /* should be undefined in initial state */);\n \n if (preCloseState === 'established') {\n try {\n if (immediate) {\n await this.closeImmediately(reason);\n } else {\n await this.closeGracefully(reason);\n }\n } catch(e) {\n debugging() && console.warn(`Warning: could not send close message to peer. connectionid=${this._id}, dcpsid=,${this.dcpsid}, url=${this.url ? this.url.href : 'unknown url'} - (${e.message})`);\n }\n }\n\n // can delete these now that we've sent the close message\n this.dcpsid = null;\n this.peerAddress = null;\n\n /* build error message */\n let rejectErr;\n if (reason instanceof Error) {\n rejectErr = reason;\n } else {\n let message;\n if (typeof reason === 'string' || reason instanceof String ) {\n message = reason;\n } else {\n if (this.url)\n message = `Connection closed (url: ${this.url}, dcpsid: ${dcpsid})`;\n else\n message = `Connection closed (dcpsid: ${dcpsid})`;\n }\n rejectErr = new DCPError(message, 'DCPC-1002');\n }\n \n if (preCloseState !== 'initial')\n {\n // Reject any pending transmissions in the message ledger\n this.messageLedger.failAllTransmissions(rejectErr);\n \n if (this.transport)\n {\n try { this.sender.shutdown(); }\n catch(e) { debugging() && console.warn(`Warning: could not shutdown sender; dcpsid=,${dcpsid}`, e); }\n\n try { this.transport.close(); }\n catch(e) { debugging() && console.warn(`Warning: could not close transport; dcpsid=,${dcpsid}`, e); }\n }\n }\n } catch(e) {\n debugging() && console.warn(`Warning: could not close connection. connectionid=${this._id}, dcpsid=,${this.dcpsid}, url=${this.url ? this.url.href : 'unknown url'} - (${e.message})`)\n }\n\n this.state.setIf('closing', 'closed');\n this.state.setIf('close-wait', 'closed');\n\n if (this.transport)\n {\n this.emit('end'); /* end event resolves promises on other threads for closeWait and close */\n }\n }\n\n /**\n * Sends a message to the connected peer. If the connection has not yet been established,\n * this routine will first invoke this.connect().\n * \n * @param {...any} args\n * @returns {Promise<Response>} a promise which resolves to a response.\n */\n async send(...args) {\n if (this.state.in(['initial','connecting'])) {\n // Original message will be rejected so we don't want a second reject\n // here since it would be uncaught.\n await this.connect().catch((error) => {\n error.message = `Connection (${this._id}) failed to connect to ${this.url ? this.url.href : '<unknown url>'}: ${error.message}`;\n throw error;\n });\n }\n\n const message = this.messageFactory.buildMessage(...args);\n\n if (this.state.in(['closing', 'closed'])) {\n let additionalInfo = \"\";\n const debugBuild = (__webpack_require__(/*! dcp/common/dcp-build */ \"./src/common/dcp-build.js\").build === 'debug');\n if (debugBuild) {\n const currentStack = new Error().stack;\n additionalInfo = `, message: ${stringify(message)}, this: ${stringify(this)}, state.lastStack ${this.state.lastStack}\\n, currentStack ${currentStack}`;\n }\n let e = new DCPError(`Connection (${this._id}) in state '${this.state}' cannot send. (url: ${this.url}${additionalInfo})`, 'DCPC-1001');\n throw e;\n }\n\n return this.sender.enqueue(message);\n }\n\n /**\n * Sends a v3-over-v4 request\n * @param url {string} v3 DcpURL\n * @param message {object} v3 \"message\" value; may be empty\n * @param authKey {object} (v3 signing|v4 authorizing) keystore, if different from identity\n * @return {Promise<object>} Resolves with v3 route's response data\n */\n async sendv3(url, message = {}, authKey = null) {\n const v3Data = {\n url,\n message,\n };\n \n const req = new this.Request('v3', v3Data);\n\n if (authKey && !this.identity.address.eq(authKey.address)) {\n await req.authorize(authKey);\n }\n \n let response = await req.send()\n\n .catch((error) => {\n return {\n success: false,\n payload: error,\n }\n });\n\n // fail at v4-protocol-level? throw it\n if (!response.success) {\n throw response.payload;\n }\n \n // fail at v3-level? also throw it\n if (response.payload.v3status === 'reject')\n throw response.payload.v3rejection;\n \n return response.payload.v3resolution;\n }\n\n /**\n * This routine returns the current time for the purposes of\n * populating the Request message payload.validity.time property.\n * \n * @returns {Number} the integer number of seconds which have elapsed since the epoch\n */\n currentTime() {\n let msSinceEpoch;\n if (this.ttl.ntp) {\n msSinceEpoch = Date.now();\n } else {\n const msSinceLastReceipt = perf.now() - this.receiver.lastResponseTiming.receivedMs;\n msSinceEpoch = this.receiver.lastResponseTiming.time * 1000 + msSinceLastReceipt;\n }\n return Math.floor(msSinceEpoch / 1000);\n }\n\n /**\n * This method sends a keepalive to the peer, and resolves when the response has been received.\n */\n keepalive() {\n return this.send('keepalive');\n }\n\n /**\n * This method checks to see a service connection is established, typically will be given a timeOut condition when invoked\n */\n async setCheckConnectionTimeout(source) {\n // failTimeout is checked to account for a race condition if the connection is established at the same time as the fail timeout expires \n let failTimeout = false;\n function checkConnectionEstablished() {\n if(failTimeout){\n failTimeout = false;\n throw new DCPError(`Connection to ${this.url} from ${source} not established within 30 seconds, instead is in state ${this.state}`, 'DCP-1006');\n }\n }\n failTimeout = setTimeout(checkConnectionEstablished.bind(this), 30000);\n this.once('connected', () => {\n if(failTimeout){\n clearTimeout(failTimeout);\n failTimeout = false;\n }\n });\n await this.connect();\n }\n\n}\n\n/**\n * Returns true if friendLocation should work in place of location from this host.\n * This allows us to transparently configure inter-daemon communication that uses\n * local LAN IPs instead of bouncing off the firewall for NAT.\n */\nasync function isFriendlyURL(url)\n{\n var remoteIp, dnsA;\n var ifaces;\n \n if (url.hostname === 'localhost')\n return true;\n\n switch(url.protocol)\n {\n case 'http:':\n case 'https:':\n case 'ws:':\n case 'tcp:':\n case 'udp:':\n case 'dcpsaw:':\n break;\n default:\n return false;\n }\n\n /* Consider same-origin match friendly */\n if (__webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\").isBrowserPlatform)\n return url.origin === window.location.origin;\n\n /* Convert an IP address to a 32-bit int in network order */\n function i32(addr)\n {\n var ret = 0;\n var octets = addr.split('.');\n\n ret |= octets[0] << 24; /* Note: JS ints are signed 32, but that doesn't matter for masking */\n ret |= octets[1] << 16;\n ret |= octets[2] << 8;\n ret |= octets[3] << 0;\n\n return ret;\n }\n \n /* Consider machines in same IPv4 subnet friendly */\n dnsA = await requireNative('dns').promises.lookup(url.hostname, {family: 4});\n if (!dnsA)\n return false;\n remoteIp = i32(dnsA.address);\n ifaces = requireNative('os').networkInterfaces();\n for (let ifaceName of Object.keys(ifaces))\n {\n for (let alias of ifaces[ifaceName])\n {\n if (alias.family != 'IPv4')\n continue;\n\n let i32_addr = i32(alias.address);\n let i32_mask = i32(alias.netmask);\n\n if ((i32_addr & i32_mask) === (remoteIp & i32_mask))\n return true;\n }\n }\n\n return false;\n}\n\n/** \n * Determine if we got the scheduler config from a secure source, eg https or local disk.\n * We assume that all https transactions have PKI-CA verified.\n *\n * @note protocol::getSchedulerConfigLocation() is populated via node-libs/config.js or dcp-client/index.js\n *\n * @returns true or falsey\n */\nfunction determineIfSecureConfig()\n{\n var schedulerConfigLocation = __webpack_require__(/*! dcp/protocol-v4 */ \"./src/protocol-v4/index.js\").getSchedulerConfigLocation();\n var schedulerConfigSecure;\n\n if (schedulerConfigLocation && (schedulerConfigLocation.protocol === 'https:' || schedulerConfigLocation.protocol === 'file:'))\n {\n debugging('strict-mode') && console.log(`scheduler config location ${schedulerConfigLocation} is secure`); /* from casual eavesdropping */\n schedulerConfigSecure = true;\n }\n\n if (isDebugBuild)\n {\n debugging('strict-mode') && console.log(`scheduler config location is always secure for debug builds`);\n schedulerConfigSecure = 'debug';\n }\n\n debugging('strict-mode') && console.debug(`Config Location ${schedulerConfigLocation} is ${!schedulerConfigSecure ? 'not secure' : 'secure-' + schedulerConfigSecure}`);\n return schedulerConfigSecure;\n}\n\n/**\n * Determine if a URL is secure by examinining the protocol, connection, and information about the \n * process; in particular, we try to determine if the dcp config was securely provided, because if \n * it wasn't, then we can't have a secure location, since the origin could be compromised.\n * \n * \"Secure\" in this case means \"secure against casual eavesdropping\", and this information should only\n * be used to refuse to send secrets over the transport or similar.\n *\n * @returns true or falsey\n */\nfunction determineIfSecureLocation(conn)\n{\n var isSecureConfig = determineIfSecureConfig();\n var secureLocation;\n\n if (!isSecureConfig) /* can't have a secure location without a secure configuration */\n return null;\n \n if (isDebugBuild || conn.url.protocol === 'https:' || conn.url.protocol === 'tcps:')\n secureLocation = true;\n else if (conn.isInitiator && conn._target.hasOwnProperty('friendLocation') && conn.url === conn._target.friendLocation)\n secureLocation = true;\n else if (conn.connectionOptions.allowUnencryptedSecrets)\n secureLocation = 'override';\n else\n secureLocation = false;\n\n debugging('strict-mode') && console.debug(`Location ${conn.url} is ${!secureLocation ? 'not secure' : 'secure-' + secureLocation}`);\n \n return secureLocation;\n}\n\nexports.Connection = Connection;\n\n\n//# sourceURL=webpack:///./src/protocol-v4/connection/connection.js?");
|
|
5448
|
+
eval("/**\n * @file protocol/connection/connection.js\n * @author Ryan Rossiter\n * @author KC Erb\n * @date January 2020, Feb 2021\n *\n * A Connection object represents a connection to another DCP entity. \n * A DCP connection may 'live' longer than the underlying protocol's connection,\n * and the underlying protocol connection (or, indeed, protocol) may change\n * throughout the life of the DCP connection.\n * \n * DCP connections are uniquely identified by the DCP Session ID, specified by\n * the dcpsid property, present in every message body. This session id negotiated during connection,\n * with the initiator and target each providing half of the string.\n *\n * \n * State Transition Diagram for Connection.state:\n *\n * initial connecting established waiting close-wait closing closed\n * ====================================================================================================\n * |- connect() ->\n * |-----------------------------------------------------------------------------------> doClose()\n * |----- connect() ----->\n * |- establishTarget() ->\n * <-- revive() --|\n * |--------- doClose() ------->\n * |- doClose() ->\n * |--------------|---------------------|-------------> failTransport()\n * |--------------|---------------------|--------------|------------|------------> <------------| doClose()\n *\n * failTransport() takes a state from anywhere, sets it to waiting,\n * and sends it back to where it came from. doclose() takes a state\n * from anywhere and sends it to the coClose() state.\n *\n * Not until the established state can we count on things like a dcpsid, \n * peerAddress, identityPromise resolution and so on.\n */\n\n\n\nconst debugging = __webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope('dcp');\nconst { EventEmitter } = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\");\nconst wallet = __webpack_require__(/*! dcp/dcp-client/wallet */ \"./src/dcp-client/wallet/index.js\");\nconst { DcpURL } = __webpack_require__(/*! dcp/common/dcp-url */ \"./src/common/dcp-url.js\");\nconst { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\nconst { assert } = __webpack_require__(/*! dcp/common/dcp-assert */ \"./src/common/dcp-assert.js\");\nconst { leafMerge, stringify } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\nconst kvin = __webpack_require__(/*! kvin */ \"./node_modules/kvin/kvin.js\");\nconst { Synchronizer } = __webpack_require__(/*! dcp/common/concurrency */ \"./src/common/concurrency.js\");\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\n\nconst perf = typeof performance === 'undefined'\n ? requireNative('perf_hooks').performance\n : performance;\n\nconst { Transport } = __webpack_require__(/*! ../transport */ \"./src/protocol-v4/transport/index.js\");\nconst { Sender } = __webpack_require__(/*! ./sender */ \"./src/protocol-v4/connection/sender.js\");\nconst { Receiver } = __webpack_require__(/*! ./receiver */ \"./src/protocol-v4/connection/receiver.js\");\nconst { MessageFactory } = __webpack_require__(/*! ./message-factory */ \"./src/protocol-v4/connection/message-factory.js\");\nconst { MessageLedger } = __webpack_require__(/*! ./message-ledger */ \"./src/protocol-v4/connection/message-ledger.js\");\nconst { ValidityStampCache } = __webpack_require__(/*! ./validity-stamp-cache */ \"./src/protocol-v4/connection/validity-stamp-cache.js\");\nconst { getGlobalIdentityCache } = __webpack_require__(/*! ./identity-cache */ \"./src/protocol-v4/connection/identity-cache.js\");\nconst { makeEBOIterator, setImmediateN } = __webpack_require__(/*! dcp/common/dcp-timers */ \"./src/common/dcp-timers.js\");\n\nconst { ConnectionMessage } = __webpack_require__(/*! ./message */ \"./src/protocol-v4/connection/message.js\");\nconst { ConnectionRequest } = __webpack_require__(/*! ./request */ \"./src/protocol-v4/connection/request.js\");\nconst { ConnectionResponse } = __webpack_require__(/*! ./response */ \"./src/protocol-v4/connection/response.js\");\nconst { ConnectionBatch } = __webpack_require__(/*! ./batch */ \"./src/protocol-v4/connection/batch.js\");\nconst { ConnectionAck } = __webpack_require__(/*! ./ack */ \"./src/protocol-v4/connection/ack.js\");\nconst { ErrorPayloadCtorFactory } = __webpack_require__(/*! ./error-payload */ \"./src/protocol-v4/connection/error-payload.js\");\nconst isDebugBuild = __webpack_require__(/*! dcp/common/dcp-build */ \"./src/common/dcp-build.js\").build === 'debug';\n\nlet globalConnectionId = 0;\n\nconst CONNECTION_STATES = [\n 'initial',\n 'connecting',\n 'established',\n 'waiting', /* Targets only, when send fails */\n 'close-wait', /* Target of close message is in this state until response is acknowledged */\n 'closing',\n 'closed',\n]\n\nclass Connection extends EventEmitter {\n static get VERSION() {\n return '5.0.0'; // Semver format\n }\n\n static get VERSION_COMPATIBILITY() {\n return '5.0.0'; // Semver format, can be a range\n }\n\n /**\n * @constructor Connection form 4:\n * Create a DCP Connection object for an initiator.\n * @param {string} target The string version (ie href) of the URL of the target to connect to.\n * @param {wallet.IdKeystore} [idKeystore]\n * @param {Object} [connectionOptions]\n * @see form 1\n */\n /**\n * @constructor Connection form 3:\n * Create a DCP Connection object for an initiator.\n * @param {DcpURL|URL} target The URL of the target to connect to.\n * @param {wallet.IdKeystore} [idKeystore]\n * @param {Object} [connectionOptions]\n * @see form 1\n */\n /**\n * @constructor Connection form 2:\n * Create a DCP Connection object for a target.\n * @param {wallet.IdKeystore} [idKeystore]\n * @param {Object} [connectionOptions]\n * @see form 1\n */\n /**\n * @constructor Connection form 1\n * Create a DCP Connection object. \n * \n * @note Connection objects exist for the lifetime of a given DCP connection \n * (session), whether or not the underlying transport (eg internet protocol) is connected or not. Once \n * the DCP session has ended, this object has no purpose and is not reusable.\n * \n * @param {object|undefined} target Undefined when we are the target, or an object describing the target. This object \n * may contain the following properties; 'location' is mandatory:\n * - location: a DcpURL that is valid from the Internet\n * - friendLocation: a DcpURL that is valid from an intranet; if\n * both location and friendLocation specified, the best\n * one will be chosen by examining IP addresses\n * - identity: a object with an address property which is an\n * instanceof wallet.Address which corresponds to the peer's\n * identity; this overrides the identity cache unless\n * connectionOptions.strict is truey.\n * @param {wallet.IdKeystore} [idKeystore] The keystore used to sign messages; used for non-repudiation.\n * If not specified, a dynamically-generated keystore will be used.\n * \n * @param {Object} [connectionOptions] Extra connection options that aren't defined via dcpConfig.dcp.connectionOptions.\n * These options include:\n * - identityUnlockTimeout: Number of (floating-point) seconds to leave the identity \n * keystore unlocked between invocations of Connection.send\n */\n constructor(target, idKeystore, connectionOptions)\n {\n /* polymorphism strategy: rewrite all to form 1 before super */\n if (target instanceof wallet.Keystore) /* form 2 */\n { \n connectionOptions = arguments[2];\n idKeystore = arguments[1];\n target = undefined;\n }\n if (typeof connectionOptions === 'undefined')\n connectionOptions = {};\n\n if (target instanceof URL) /* form 3.2 */\n target = { location: new DcpURL(target) };\n else if (DcpURL.isURL(target)) /* form 3.1 */\n target = { location: new DcpURL(target) };\n else if (target instanceof String || typeof target === 'string') /* form 4 */\n target = { location: new DcpURL(target) };\n\n assert((typeof target === 'undefined') || (typeof target === 'object' && DcpURL.isURL(target.location)));\n assert(typeof connectionOptions === 'object');\n \n super(`Protocol Connection (${target ? 'initiator' : 'target'})`);\n\n if (target) {\n this.isInitiator = true;\n this.debugLabel = 'connection(i):';\n this._target = target;\n } else {\n this.isInitiator = false;\n this.debugLabel = 'connection(t):';\n }\n\n if (idKeystore) {\n this.identityPromise = (async () => await idKeystore)();\n } else {\n this.identityPromise = wallet.getId();\n }\n\n this.identityPromise.then((keystore) => {\n this.identity = keystore;\n debugging('connection') && console.debug(this.debugLabel, 'identity is', keystore.address);\n return keystore;\n });\n\n this.ttl = {\n ntp: false,\n }\n \n // Init internal state / vars\n this.state = new Synchronizer(CONNECTION_STATES[0], CONNECTION_STATES);\n this.state.on('change', (s) => this.emit('readyStateChange', s) );\n\n this._id = globalConnectionId++;\n this.debugLabel = this.debugLabel.replace(')', `#${this._id})`);\n debugging('connection') && console.debug(this.debugLabel, 'connection id is', this._id, `target is ${target && target.location}`);\n this.dcpsid = null;\n this.peerAddress = null;\n this.transport = null;\n this.maxConnectionTimeout = null;\n this.messageFactory = new MessageFactory(this);\n this.messageLedger = new MessageLedger(this);\n this.authorizedSender = null;\n \n this.Message = ConnectionMessage(this);\n this.Request = ConnectionRequest(this.Message);\n this.Response = ConnectionResponse(this.Message);\n this.Batch = ConnectionBatch(this.Message);\n this.Ack = ConnectionAck(this.Message);\n this.ErrorPayload = ErrorPayloadCtorFactory(this);\n this.connectTime = Date.now();\n\n this.openRequests = {};\n ValidityStampCache.init();\n\n this.receiver = new Receiver(this, this.messageLedger);\n this.receiver.on('request', (req) => this.emit('request', req));\n\n debugging('connection') && console.debug(this.debugLabel, `new; ${target && target.location || '<target>'}`);\n\n this.configureConnectionForUrl = (url) => {\n if (typeof connectionOptions.ttl === 'number')\n connectionOptions = Object.assign({}, connectionOptions, { ttl: { default: connectionOptions.ttl } });\n \n this.url = url;\n this.connectionOptions = leafMerge(/* ordered from least to most specific */\n dcpConfig.dcp.connectionOptions.default,\n this.url && dcpConfig.dcp.connectionOptions[this.url.hostname],\n this.url && dcpConfig.dcp.connectionOptions[this.url.origin],\n dcpConfig.dcp.connectionOptions[this.isInitiator ? this.url.href : 'target'],\n connectionOptions\n );\n\n // ensure we're using a _copy_ of the transports list, not mutating the original\n this.connectionOptions.transports = [...this.connectionOptions.transports];\n this.originalConnectionTransports = [].concat(this.connectionOptions.transports);\n this.transportsTried = 0;\n\n this.ttl = leafMerge(this.ttl, this.connectionOptions.ttl);\n this.unlockTimeout = this.connectionOptions.identityUnlockTimeout;\n this.connectionOptions.id = this._id;\n this.EBOIterator = makeEBOIterator(1000, 60000);\n\n assert(this.unlockTimeout >= 0);\n assert(typeof this.connectionOptions.ttl.min === 'number');\n assert(typeof this.connectionOptions.ttl.max === 'number');\n assert(typeof this.connectionOptions.ttl.default === 'number');\n\n this.secureLocation = determineIfSecureLocation(this);\n }\n\n /* By default, unsent messages cause .send() to reject for DCP intiators, but not targets. When\n * messages are unsent but not rejected, the send promise resolves with an instance of Error.\n *\n * Note: \"unsent messages\" are messages we tried to send, but couldn't be verified as sent because \n * the connection closed. It is plausible that they reached the other end, but also plausible that\n * they did not.\n */\n this.rejectUnsentMessages = this.isInitiator;\n }\n\n /**\n * This method is an instantiator/factory function for building a connection\n * that will act as the target in a new protocol connection. It's a little\n * like making a new connection and calling `connect` on it, except that\n * instead of having a url to connect to we have a transport which should\n * be ready to emit the connect message from the initiator.\n * \n * @param {wallet.Keystore} ks - Keystore to associate to the new connection.\n */\n static async newTarget(url, ks, transport) {\n const target = new Connection(undefined, ks, {\n ttl: {\n ntp: true, // targets should always be NTP-synced\n }\n });\n\n target.configureConnectionForUrl(url);\n assert(target.connectionOptions.ttl.ntp);\n \n await target.identityPromise;\n target.transport = transport;\n target.sender = new Sender(target);\n target.state.set('initial', 'connecting');\n return target;\n }\n\n /**\n * When invoked by the initator, this method establishes the connection by connecting\n * to the target provided to the constructor.\n * \n * When a dcpConfig-fragment is used as the target object, we determine automatically if \n * we should be using the location or the friendLocation property at this point.\n *\n * This function is also where the connectionOptions are merged together. This means \n * that if either of these objects come from dcpConfig and we HUP a service running this\n * code, that the new configuration will be used to connect, instead of the configuration \n * that was defined at the time of construction.\n *\n * @throws when called by a target, or when called more than once\n */\n async connect() {\n var presharedPeerAddress;\n \n let transportName;\n \n if (!this.state.in(['initial', 'connecting'])) {\n throw new Error(`Can only invoke connect in initial/connecting state. Current state: ${this.state.valueOf()}.`, );\n }\n\n // make connect re-entrant if called during the connecting phase\n if (this.state.is('connecting'))\n {\n /* nth connect attempt on same Connection instance before actual connection -- swallow attempts and wait until connected */\n do\n {\n await this.state.until(['established', 'close-wait', 'closing', 'closed']);\n } while(this.state.is('waiting'));\n\n if (!this.state.is('established'))\n throw new Error(`Could not establish connection to ${this.url}; in state ${this.state}`);\n return;\n }\n\n this.state.set('initial', 'connecting');\n\n if (this._target && this._target.hasOwnProperty('friendLocation') && await isFriendlyURL(this._target.friendLocation))\n this.configureConnectionForUrl(this._target.friendLocation);\n else\n this.configureConnectionForUrl(this._target.location);\n\n const transport = this.fetchTransport();\n this.transport = transport.transport;\n transportName = transport.moduleName;\n this.sender = new Sender(this);\n // create sender before promises so that we can still enqueue messages before hopping off the event loop\n await this.identityPromise;\n await this.connectToTarget(transportName);\n const establishResults = await this.sender.establish().catch( (err) => {\n console.error('Failed to establish connection.');\n this.close(err, true);\n throw err;\n });\n const dcpsid = establishResults.dcpsid;\n const peerAddress = wallet.Address(establishResults.peerAddress);\n\n if (!this.connectionOptions.strict && this._target.identity)\n {\n if (determineIfSecureConfig())\n {\n let identity = await this._target.identity;\n if (!(identity instanceof wallet.Keystore))\n identity = { address: new wallet.Address(identity) }; /* map strings and Addresses to ks ducks */\n\n presharedPeerAddress = identity.address;\n debugging('connection') && console.debug('Using preshared peer address', presharedPeerAddress);\n }\n }\n this.ensureIdentity(peerAddress, presharedPeerAddress); /** XXXwg possible resource leak: need cleanup; need try {} catch->emit(cleanup) */\n \n // checks have passed, now we can set props\n this.peerAddress = peerAddress;\n if (this.dcpsid)\n throw new DCPError(`Reached impossible state in connection.js; dcpsid already specified ${this.dcpsid} (${this.url})`, 'DCPC-1004');\n this.dcpsid = dcpsid;\n\n // Update state\n this.state.set('connecting', 'established'); /* established => dcpsid has been set */\n this.emit('connected', this.url);\n this.sender.finishSetup();\n }\n\n moreTransportsExist() {\n return this.connectionOptions\n && this.connectionOptions.transports\n && Array.isArray(this.connectionOptions.transports)\n && this.connectionOptions.transports.length > 0;\n }\n \n /**\n * Initiators only.\n * Try to get and establish a transport from the transports list.\n * @returns {Transport} a new transport instance or false if no transports are left to try\n */\n fetchTransport() {\n //We want to cycle our possible transports until the maxConnectionTimeout runs out or we have a connection\n const moduleName = this.connectionOptions.transports.shift();\n this.connectionOptions.transports.push(moduleName);\n this.transportsTried++;\n debugging() && console.log(this.debugLabel, 'fetchTransport trying ', moduleName, `remaining transports: ${this.connectionOptions.transports.length}`)\n if (!moduleName) {\n console.error('Error: All transports have failed to connect/transmit within limits. Closing connection.');\n this.close('all transports failed', true).catch(error => {\n console.error(`Error: failed to close transport:`, error);\n });\n throw new DCPError(`Failed to connect to ${this.url}, no transport could be established`, 'DCPC-ENOTRANSPORT');\n }\n const TransportClass = Transport.require(moduleName);\n const transport = new TransportClass(null, this.connectionOptions);\n transport.on('message', (m) => {\n this.handleMessage(JSON.parse(m))\n });\n return { transport, moduleName };\n }\n\n /**\n * Initiators only\n * Resolves when we have connected to target using a transport.\n * Calls \"failTransport\" if we run out of time / attempts to connect.\n * Let's transport handle failure but also is watching for it too.\n */\n async connectToTarget(currentTransport) {\n debugging('connection') && console.log(this.debugLabel, `Using transport \"${this.transport.name}\" ... connecting to ${this.url}.`);\n const globalWaitMs = dcpConfig.dcp.maxConnectionTimeout;\n const connectWaitMs = this.transport.connect(this.url, this.connectionOptions);\n\n this.maxConnectionTimeout = this.maxConnectionTimeout ? this.maxConnectionTimeout\n : setTimeout(this.close.bind(this, `Timeout to have a transport connect reached at ${globalWaitMs} ms`, true), globalWaitMs);\n\n await new Promise( (resolve) => {\n // Listener for a connect-failed, will invoke EBO algorithm to retry the connection\n this.transport.once('connect-failed', async () => {\n if (this.transportsTried >= this.connectionOptions.transports.length)\n {\n this.transportsTried = 0;\n const backoffTime = this.EBOIterator.next().value;\n debugging('connection') && console.debug(this.debugLabel, 'connecting in', backoffTime / 1000, 'seconds');\n await this.failTransport('Connect-failed event handler fired.', backoffTime);\n }\n else\n {\n await this.failTransport('Connect-failed event handler fired.');\n }\n resolve();\n });\n\n this.transport.once('connected', () => {\n clearTimeout(this.maxConnectionTimeout);\n this.maxConnectionTimeout = null;\n /* When we successfully connect a transport, we know that transport has potential to consistently work,\n * and disconnects may happen for unrelated reason (such as switching wifi causing a disconnect). \n * Thus, we recreate our transport options list with this transport at the top\n */\n this.connectionOptions.transport = [currentTransport].concat(this.originalConnectionTransports);\n resolve();\n });\n });\n this.transport.on('reconnect', () => {\n debugging('connection') && console.log(this.debugLabel, `Transport reconnected to ${this.url}`);\n /**\n * This may not work at the moment.\n * We don't want to naively send a keepalive because it might go to a closing/closed connection and throw an exception.\n * DCP-2528 is the bug for removing the keepalive in the reconnect handler until we can do it right.\n * DCP-2527 is the bug for making the reconnect handler work properly.\n */\n this.emit('reconnect');\n });\n }\n\n /**\n * Target gains full status once dcpsid and peerAddress\n * are provided by first connect request.\n * @param {string} dcpsid dcpsid\n * @param {wallet.Address} peerAddress Address of peer\n */\n establishTarget(dcpsid, peerAddress) {\n this.connectResponseId = Symbol(); // un-register ConnectResponse\n this.peerAddress = peerAddress;\n if (this.dcpsid)\n throw new DCPError(`Reached impossible state in connection.js; dcpsid already specified ${this.dcpsid}!=${dcpsid} (${this.url})`, 'DCPC-1005');\n this.dcpsid = dcpsid;\n this.state.set('connecting', 'established'); /* established => dcpsid has been set */\n }\n\n ensureIdentity (peerAddress, presharedPeerAddress)\n {\n let idc = getGlobalIdentityCache();\n let noConflict = idc.learnIdentity(this.url, peerAddress, presharedPeerAddress);\n\n if (!noConflict)\n throw new DCPError(`**** Security Error: Identity address ${peerAddress} does not match the saved key for ${this.url}`, 'DCPC-EADDRCHANGE');\n }\n\n /**\n * Check that the transport has given us a message worth dealing with then\n * either let the receiver handle it (message) or the message ledger (ack).\n * @param {object} message parsed (but not validated) message object\n */\n async handleMessage (message) {\n var validation;\n var messageError;\n var messageValid = true;\n \n if (this.state.is('closed')) {\n debugging('connection') && console.warn(this.debugLabel, 'handleMessage was called on a closed connection.');\n return;\n }\n /**\n * We always ack a duplicate transmission.\n * This must happen before validation since during startup we may lack a\n * nonce or dcpsid (depending on whether initiator or target + race).\n */\n if (this.isDuplicateTransmission(message)) {\n debugging('connection') && console.debug(this.debugLabel, 'duplicate message:', message.body);\n this.transport.send(this.lastAckSigned);\n return;\n }\n\n /* Capture the initial identity of the remote end during the connect operation */\n if (this.authorizedSender === null)\n {\n let messageBody = message.body;\n let payload = messageBody.payload;\n \n if (payload && message.body.type === 'batch')\n {\n for (let i=0; i < payload.length; i++)\n {\n let innerMessageBody = payload[i];\n\n if (innerMessageBody.payload && innerMessageBody.payload.operation === 'connect' && (innerMessageBody.type === 'response' || innerMessageBody.type === 'request'))\n {\n messageBody = innerMessageBody;\n payload = innerMessageBody.payload;\n break;\n }\n }\n }\n\n if (payload)\n {\n if (payload.operation === 'connect' && (messageBody.type === 'response' || messageBody.type === 'request'))\n this.authorizedSender = message.owner;\n else\n throw new DCPError('Message payload received before connection operation', 'DCPC-1007');\n }\n }\n else\n {\n if (message.owner !== this.authorizedSender)\n {\n messageError = new DCPError('Message came from invalid sender.', 'DCPC-1002');\n debugging('connection') && console.debug(this.debugLabel, 'Message owner was not an authorized sender - aborting connection');\n this.close(messageError, true);\n this.emit('error', messageError);\n return;\n }\n }\n\n if (!this.isInitiator && this.state.in(['connecting']))\n {\n // while connecting, the target gets its nonce from the initiator\n if (!this.isInitiator) this.sender.nonce = message.body.nonce;\n }\n\n validation = this.validateSignature(message);\n if (validation.success !== true)\n {\n messageError = new DCPError(`invalid message: ${validation.errorMessage}`, 'DCPC-1002');\n debugging('connection') && console.debug(this.debugLabel, 'Message failed validation -', validation.errorMessage);\n this.close(messageError, true);\n messageValid = false;\n }\n\n if (message.body.type === 'unhandled-message')\n {\n /* This special message type may not have a dcpsid, peerAddress, etc., so it might not\n * validate. It is also not a \"real\" message and only used to report ConnectionManager routing \n * errors, so we just report here, drop it, and close the connection.\n *\n * Note also that this is probably the wrong way to handle this case - restarting daemons - but\n * that is a problem for another day. /wg nov 2021\n */\n let messageError = new DCPError(`target could not process message (${message.payload && message.payload.name || 'unknown error'})`,'DCPC-1002');\n debugging('connection') && console.warn(this.debugLabel, \"Target Error - target could not process message.\", JSON.stringify(message.body),\n \"Aborting connection.\");\n this.close(messageError, true);\n messageValid = false;\n }\n\n validation = this.validateMessage(message);\n if (validation.success !== true)\n {\n let messageError = new DCPError(`invalid message: ${validation.errorMessage}`, 'DCPC-1002');\n debugging('connection') && console.debug(this.debugLabel, 'Message failed validation -', validation.errorMessage);\n this.close(messageError, true);\n messageValid = false;;\n }\n\n if (!messageValid) {\n message.body.type = 'unhandled-message'\n this.emit('error', messageError);\n }\n \n if (message.body.type === \"ack\") {\n const ack = new this.Ack(message.body);\n this.messageLedger.handleAck(ack);\n return;\n } else if (message.body.type !== 'unhandled-message') {\n this.lastMessage = message;\n await this.ackMessage(message);\n }\n \n this.receiver.handleMessage(message);\n }\n\n async ackMessage(message) {\n debugging('connection') && console.debug(this.debugLabel, 'acking message.');\n const ack = new this.Ack(message);\n const signedMessage = await ack.sign(this.identity);\n this.transport.send(signedMessage);\n this.lastAck = ack;\n this.lastAckSigned = signedMessage;\n }\n\n /**\n * Checks if the batch we just received has the same nonce\n * as the most-recently received batch.\n * @param {object} messageJSON\n */\n isDuplicateTransmission(messageJSON) {\n return this.lastMessage && this.lastMessage.body.nonce === messageJSON.body.nonce;\n }\n\n /**\n * Validate that the signature was generated from this message body\n * @param {Object} message\n * @returns {Object} with properties 'success' and 'errorMessage'. When the message is valid on its \n * face, the success property is true, otherwise it is is false. When it is false,\n * the errorMessage property will be a string explaining why.\n */\n validateSignature(message)\n {\n if (!message.signature) {\n debugging('connection') && console.warn(\"Message does not have signature, aborting connection\");\n return { success: false, errorMessage: \"message is missing signature\" };\n }\n \n const owner = new wallet.Address(message.owner);\n const signatureValid = owner.verifySignature(message.body, message.signature);\n\n if (!signatureValid)\n {\n debugging('connection') && console.warn(\"Message has an invalid signature, aborting connection\");\n return { success: false, errorMessage: \"invalid message signature\" };\n }\n\n return { success: true };\n }\n \n /**\n * This method is used to perform validation on all types of messages.\n * It validates the DCPSID, nonce, and the peerAddress.\n * @param {Object} message\n * @returns {Object} with properties 'success' and 'errorMessage'. When the message is valid on its \n * face, the success property is true, otherwise it is is false. When it is false,\n * the errorMessage property will be a string explaining why.\n *\n */\n validateMessage(message)\n {\n try\n {\n if (this.peerAddress && !this.peerAddress.eq(message.owner))\n {\n debugging('connection') && console.warn(\"Received message's signature address does not match peer address, aborting connection\\n\",\n \"(signature addr)\", message.owner, '\\n',\n \"(peer addr)\", this.peerAddress);\n return { success: false, errorMessage: \"received message signature does not match peer address\" };\n }\n\n if (this.state.in(['established', 'closing', 'close-wait']) && message.body.type !== 'unhandled-message')\n {\n const body = message.body;\n\n assert(this.peerAddress); /* should be set in connect */\n /**\n * Security note:\n * We don't require the dcpsid to match on an ack because the connect response\n * ack doesn't have a dcpsid until after it is processed. Also ack's are protected\n * by ack tokens and signatures, so this doesn't leave a hole, just an inconsistency.\n */\n if (body.type !== 'ack' && body.dcpsid !== this.dcpsid)\n {\n debugging('connection') && console.warn(\"Received message's DCPSID does not match, aborting connection\\n\",\n \"Message owner:\", message.owner, '\\n',\n \"(ours)\", this.dcpsid, (Date.now() - this.connectTime)/1000, \"seconds after connecting - state:\", this.state._, \"\\n\", \n \"(theirs)\", body.dcpsid);\n if(body.dcpsid.substring(0, body.dcpsid.length/2) !== this.dcpsid.substring(0, this.dcpsid.length/2)){\n debugging('connection') && console.warn(\" Left half of both DCPSID is different\");\n }\n if(body.dcpsid.substring(body.dcpsid.length/2 + 1, body.dcpsid.length) !== this.dcpsid.substring(this.dcpsid.length/2 + 1, body.dcpsid.length)){\n debugging('connection') && console.warn(\" Right half of both DCPSID is different\");\n }\n return { success: false, errorMessage: \"DCPSID do not match\" };\n }\n\n if (body.type !== 'ack' && this.lastAck.nonce !== body.nonce)\n {\n debugging('connection') && console.warn(\"Received message's nonce does not match expected nonce, aborting connection\\n\");\n debugging('connection') && console.debug(this.debugLabel, this.lastAck.nonce, body.nonce);\n return { success: false, errorMessage: \"received message's nonce does not match expected nonce\" };\n }\n }\n\n return { success: true };\n }\n catch(error)\n {\n console.error('message validator failure:', error);\n return { success: false, errorMessage: 'validator exception ' + error.message };\n }\n\n return { success: false, errorMessage: 'impossible code reached' };\n }\n\n /**\n * Targets Only.\n * The receiver creates a special connect response and the connection\n * needs to know about it to get ready for the ack. See `isWaitingForAck`.\n * @param {Message} message message we are sending out and waiting to\n * ack'd, probably a batch containing the response.\n */\n registerConnectResponse(message) {\n this.connectResponseId = message.id;\n }\n\n /**\n * Targets only\n * During the connection process a target sends a connect\n * response to an initiator and the initiator will ack it. Since transports\n * are not tightly coupled, we have no authoritative way to route the ack back\n * to the right connection. So a connection briefly registers the ack it\n * is looking for in this case. It will formally validate the ack after routing.\n * @param {string} messageId id of the message this ack is acknowledging.\n */\n isWaitingForAck(messageId) {\n return messageId === this.connectResponseId;\n }\n\n /**\n * Targets: nothing to do, we are waiting for initiator.\n * Initiators: give up on current transport and try next available option.\n * For now we just have socketio so that means we throw an error.\n * @return {Promise<boolean>} indicating whether we were able to find a new transport.\n */\n async failTransport (failureDescription, retryWaitTime = 0) {\n let transportName;\n const preWaitingState = this.state.valueOf(); /* XXXwg bogus - need explicit and reasonable source states */\n\n /* If we are already in process of closing or already waiting, we don't want to beat \n * the dead horse. Let it stay dead with no transport\n */\n if (['closing', 'closed', 'close-wait', 'waiting'].includes(preWaitingState))\n return;\n\n this.state.testAndSet(preWaitingState, 'waiting');\n debugging('connection') && console.log(this.debugLabel, `Transport \"${this.transport.name}\" has failed.`);\n \n if (this.isInitiator) {\n if (!this.moreTransportsExist()) {\n console.error('Transport failed:', failureDescription);\n // fetchTransport will immediately fail\n }\n debugging('connection') && console.log(this.debugLabel, `Attempting to use next transport.`);\n this.transport.close();\n const transport = this.fetchTransport();\n this.transport = transport.transport;\n transportName = transport.moduleName;\n \n await new Promise(r => setTimeout(r, retryWaitTime));\n \n if (['closing', 'closed', 'close-wait'].includes(this.state.valueOf())) // If some other process closes the connection, don't try to reconnect\n return;\n\n this.state.set('waiting', preWaitingState);\n await this.connectToTarget(transportName);\n }\n }\n\n /**\n * Targets only\n * Give target another transport to try sending messages on again.\n */\n revive(transport) {\n this.transport = transport;\n this.state.set('waiting', 'established');\n }\n\n /**\n * Put connection into close-wait state so that a call to `close`\n * in this state will *not* trigger sending a `close` message to the peer.\n * Then call close.\n *\n * @note: This function is called when the remote end of the transport sends\n * a close command\n */\n closeWait (errorCode = null)\n {\n const preCloseState = this.state.valueOf();\n var reason;\n \n debugging('connection') && console.debug(this.debugLabel, `responding to close. state=closeWait dcpsid=${this.dcpsid}`);\n\n if (this.state.is('closed'))\n {\n debugging('connection') && console.debug(this.debugLabel, `remote asked us to close a closed connection; dcpsid=${this.dcpsid}`);\n return;\n }\n\n // continue with close in either case\n reason = 'Received close from peer with Error Code ${errorCode}';\n if (!this.isInitiator)\n reason += ` (${this.url})`;\n else\n reason += ` (${this.debugLabel}${this.peerAddress.address})`;\n\n reason = new DCPError(reason, errorCode || 'DCPC-1011');\n\n // If we're already closing, wait for it to complete then resolve\n // WARNING: any place we transition to closing or close-wait, we MUST guarantedd\n // that 'end' will be emitted, or this code will hang forever!\n if (this.state.in(['close-wait', 'closing'])) {\n return new Promise((resolve) => {\n this.once('end', resolve) /* eventually fired by doClose elsewhere */\n });\n }\n\n if (this.state.is('closed')) /* closed somehow on us during await */\n return;\n\n this.state.set(preCloseState, 'close-wait');\n return this.doClose(preCloseState, reason, true);\n }\n\n /**\n * This method will begin gracefully closing the protocol connection.\n * It will only close after sending all pending messages.\n * \n * @param {string|Error} [reason] Either an Error or a message to use in the Error that will reject pending sends.\n * @param {boolean} [immediate] If true, does not wait to send messages or the `close` request.\n *\n * @return a Promise which resolves when the connection has been confirmed closed and the end event has been fired.\n */\n close (reason=null, immediate=false)\n {\n if (this.state.is('closed')) return Promise.resolve();\n\n const preCloseState = this.state.valueOf();\n debugging('connection') && \n console.debug(this.debugLabel, \n `close; dcpsid=${this.dcpsid} state=${preCloseState} reason=${reason} immediate=${immediate} stack=${new Error().stack}`);\n\n // If we're already closing, wait for it to complete then resolve\n if (this.state.in(['close-wait', 'closing'])) {\n return new Promise((resolve) => {\n this.once('end', resolve)\n });\n }\n\n // Put in closing state no matter the current state\n this.state.set(preCloseState, 'closing');\n\n // Perform actual work of closing\n return this.doClose(preCloseState, reason, immediate);\n }\n\n /**\n * sends close message to peer and waits for response\n * @note: This function is not reentrant!\n */\n async closeGracefully(reason) {\n if (this.transport)\n {\n /* If we got as far as initializing a transport during connect(), send close\n * message to peer, should get a response before time is up.\n */\n const closeMessage = this.messageFactory.buildMessage('close');\n if (reason instanceof Error)\n closeMessage.payload.errorCode = reason.code;\n this.sender.enqueue(closeMessage)\n await new Promise(r => setImmediateN(r, 30));\n this.messageLedger.fulfillMessagePromise(closeMessage.id, {});\n }\n }\n\n /** sends close message to peer but doesn't require response \n */\n async closeImmediately(reason) {\n if(this.sender.inFlight)\n this.sender.clearFlightDeck(this.sender.inFlight.message, this.sender.nonce);\n \n let closeMessage = this.messageFactory.buildMessage('close');\n if (reason instanceof Error)\n closeMessage.payload.errorCode = reason.code;\n closeMessage.ackToken = this.sender.makeAckToken();\n closeMessage.nonce = this.sender.nonce;\n let signedCloseMessage = await closeMessage.sign();\n \n this.sender.inFlight = { message: closeMessage, signedMessage: signedCloseMessage };\n debugging('connection') && console.debug(this.debugLabel, 'sending close message to peer');\n this.transport.send(signedCloseMessage).catch(error => console.info(this.debugLabel, 'failed to send close message to peer ${this.peerAddress.address}', error.message));\n }\n \n /**\n * Core close functionality shared by `close` and `closeWait`\n *\n * @param {string} preCloseState the state that the connection was in at the start of the\n * invocation of close() or closeWait()\n *\n * @note: this function is not reentrant due to closeGracefully\n */\n async doClose(preCloseState, reason, immediate) {\n const dcpsid = this.dcpsid;\n\n try\n { \n // Emit the close event the moment we know we are going to close, \n // so we can catch the close event and reopen the connection\n this.emit('close', dcpsid /* should be undefined in initial state */);\n\n assert(this.state.valueOf() === 'closing' || this.state.valueOf() === 'close-wait');\n if (preCloseState === 'established' && !immediate) {\n try {\n if (immediate) {\n await this.closeImmediately(reason);\n } else {\n await this.closeGracefully(reason);\n }\n } catch(e) {\n debugging() && console.warn(`Warning: could not send close message to peer. connectionid=${this._id}, dcpsid=,${this.dcpsid}, url=${this.url ? this.url.href : 'unknown url'} - (${e.message})`);\n }\n }\n\n // can delete these now that we've sent the close message\n this.dcpsid = null;\n this.peerAddress = null;\n\n /* build error message */\n let rejectErr;\n if (reason instanceof Error) {\n rejectErr = reason;\n } else {\n let message;\n if (typeof reason === 'string' || reason instanceof String ) {\n message = reason;\n } else {\n if (this.isInitiator)\n message = `Connection closed (url: ${this.url}, dcpsid: ${dcpsid})`;\n else\n message = `Connection closed (peer: ${this.peerAddress.address} dcpsid: ${dcpsid})`;\n }\n rejectErr = new DCPError(message, 'DCPC-1002');\n }\n \n if (preCloseState !== 'initial')\n {\n // Reject any pending transmissions in the message ledger\n this.messageLedger.failAllTransmissions(rejectErr);\n \n if (this.transport)\n {\n try { this.sender.shutdown(); }\n catch(e) { debugging() && console.warn(`Warning: could not shutdown sender; dcpsid=,${dcpsid}`, e); }\n\n try { this.transport.close(); }\n catch(e) { debugging() && console.warn(`Warning: could not close transport; dcpsid=,${dcpsid}`, e); }\n }\n }\n } catch(e) {\n debugging() && console.warn(`Warning: could not close connection. connectionid=${this._id}, dcpsid=,${this.dcpsid}, url=${this.url ? this.url.href : 'unknown url'} - (${e.message})`)\n }\n\n this.state.setIf('closing', 'closed');\n this.state.setIf('close-wait', 'closed');\n\n if (this.transport)\n {\n this.emit('end'); /* end event resolves promises on other threads for closeWait and close */\n }\n }\n\n /**\n * Sends a message to the connected peer. If the connection has not yet been established,\n * this routine will first invoke this.connect().\n * \n * @param {...any} args\n * @returns {Promise<Response>} a promise which resolves to a response.\n */\n async send(...args) {\n \n if (this.state.in(['initial','connecting']))\n {\n // Original message will be rejected so we don't want a second reject\n // here since it would be uncaught.\n const stackMemo = new Error('stackMemo').stack;\n\n await this.connect().catch( e => {\n let error = new DCPError(`Connection (${this._id}) failed to connect to ${this.url ? this.url.href : '<unknown url>'}: ${e.message}`, e.code);\n error.stack += e.stack .replace(/^[^\\n]*\\n[^\\n]*/, '\\n --------------------');\n error.stack += stackMemo.replace(/^[^\\n]*\\n[^\\n]*/, '\\n --------------------');\n throw error;\n });\n }\n\n const message = this.messageFactory.buildMessage(...args);\n\n if (this.state.in(['closing', 'closed']))\n {\n let peerInfo = this.isInitiator ? this.url : this.debugInfo + this.peerAddress.address;\n let error = new DCPError(`Connection (${this._id}) in state '${this.state}' cannot send. (${peerInfo})`, 'DCPC-1001');\n\n throw error;\n }\n\n return this.sender.enqueue(message);\n }\n\n /**\n * Sends a v3-over-v4 request\n * @param url {string} v3 DcpURL\n * @param message {object} v3 \"message\" value; may be empty\n * @param authKey {object} (v3 signing|v4 authorizing) keystore, if different from identity\n * @return {Promise<object>} Resolves with v3 route's response data\n */\n async sendv3(url, message = {}, authKey = null) {\n const v3Data = {\n url,\n message,\n };\n \n const req = new this.Request('v3', v3Data);\n\n if (authKey && !this.identity.address.eq(authKey.address)) {\n await req.authorize(authKey);\n }\n \n let response = await req.send()\n\n .catch((error) => {\n return {\n success: false,\n payload: error,\n }\n });\n\n // fail at v4-protocol-level? throw it\n if (!response.success) {\n throw response.payload;\n }\n \n // fail at v3-level? also throw it\n if (response.payload.v3status === 'reject')\n throw response.payload.v3rejection;\n \n return response.payload.v3resolution;\n }\n\n /**\n * This routine returns the current time for the purposes of\n * populating the Request message payload.validity.time property.\n * \n * @returns {Number} the integer number of seconds which have elapsed since the epoch\n */\n currentTime() {\n let msSinceEpoch;\n if (this.ttl.ntp) {\n msSinceEpoch = Date.now();\n } else {\n const msSinceLastReceipt = perf.now() - this.receiver.lastResponseTiming.receivedMs;\n msSinceEpoch = this.receiver.lastResponseTiming.time * 1000 + msSinceLastReceipt;\n }\n return Math.floor(msSinceEpoch / 1000);\n }\n\n /**\n * This method sends a keepalive to the peer, and resolves when the response has been received.\n */\n keepalive() {\n return this.send('keepalive');\n }\n\n /**\n * This method checks to see a service connection is established, typically will be given a timeOut condition when invoked\n */\n async setCheckConnectionTimeout(source) {\n // failTimeout is checked to account for a race condition if the connection is established at the same time as the fail timeout expires \n let failTimeout = false;\n function checkConnectionEstablished() {\n if(failTimeout){\n failTimeout = false;\n throw new DCPError(`Connection to ${this.url} from ${source} not established within 30 seconds, instead is in state ${this.state}`, 'DCPC-1006');\n }\n }\n failTimeout = setTimeout(checkConnectionEstablished.bind(this), 30000);\n this.once('connected', () => {\n if(failTimeout){\n clearTimeout(failTimeout);\n failTimeout = false;\n }\n });\n await this.connect();\n }\n\n}\n\n/**\n * Returns true if friendLocation should work in place of location from this host.\n * This allows us to transparently configure inter-daemon communication that uses\n * local LAN IPs instead of bouncing off the firewall for NAT.\n */\nasync function isFriendlyURL(url)\n{\n var remoteIp, dnsA;\n var ifaces;\n \n if (url.hostname === 'localhost')\n return true;\n\n switch(url.protocol)\n {\n case 'http:':\n case 'https:':\n case 'ws:':\n case 'tcp:':\n case 'udp:':\n case 'dcpsaw:':\n break;\n default:\n return false;\n }\n\n /* Consider same-origin match friendly */\n if (__webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\").isBrowserPlatform)\n return url.origin === window.location.origin;\n\n /* Convert an IP address to a 32-bit int in network order */\n function i32(addr)\n {\n var ret = 0;\n var octets = addr.split('.');\n\n ret |= octets[0] << 24; /* Note: JS ints are signed 32, but that doesn't matter for masking */\n ret |= octets[1] << 16;\n ret |= octets[2] << 8;\n ret |= octets[3] << 0;\n\n return ret;\n }\n \n /* Consider machines in same IPv4 subnet friendly */\n dnsA = await requireNative('dns').promises.lookup(url.hostname, {family: 4});\n if (!dnsA)\n return false;\n remoteIp = i32(dnsA.address);\n ifaces = requireNative('os').networkInterfaces();\n for (let ifaceName of Object.keys(ifaces))\n {\n for (let alias of ifaces[ifaceName])\n {\n if (alias.family != 'IPv4')\n continue;\n\n let i32_addr = i32(alias.address);\n let i32_mask = i32(alias.netmask);\n\n if ((i32_addr & i32_mask) === (remoteIp & i32_mask))\n return true;\n }\n }\n\n return false;\n}\n\n/** \n * Determine if we got the scheduler config from a secure source, eg https or local disk.\n * We assume that all https transactions have PKI-CA verified.\n *\n * @note protocol::getSchedulerConfigLocation() is populated via node-libs/config.js or dcp-client/index.js\n *\n * @returns true or falsey\n */\nfunction determineIfSecureConfig()\n{\n var schedulerConfigLocation = __webpack_require__(/*! dcp/protocol-v4 */ \"./src/protocol-v4/index.js\").getSchedulerConfigLocation();\n var schedulerConfigSecure;\n\n if (schedulerConfigLocation && (schedulerConfigLocation.protocol === 'https:' || schedulerConfigLocation.protocol === 'file:'))\n {\n debugging('strict-mode') && console.log(`scheduler config location ${schedulerConfigLocation} is secure`); /* from casual eavesdropping */\n schedulerConfigSecure = true;\n }\n\n if (isDebugBuild)\n {\n debugging('strict-mode') && console.log(`scheduler config location is always secure for debug builds`);\n schedulerConfigSecure = 'debug';\n }\n\n debugging('strict-mode') && console.debug(`Config Location ${schedulerConfigLocation} is ${!schedulerConfigSecure ? 'not secure' : 'secure-' + schedulerConfigSecure}`);\n return schedulerConfigSecure;\n}\n\n/**\n * Determine if a URL is secure by examinining the protocol, connection, and information about the \n * process; in particular, we try to determine if the dcp config was securely provided, because if \n * it wasn't, then we can't have a secure location, since the origin could be compromised.\n * \n * \"Secure\" in this case means \"secure against casual eavesdropping\", and this information should only\n * be used to refuse to send secrets over the transport or similar.\n *\n * @returns true or falsey\n */\nfunction determineIfSecureLocation(conn)\n{\n var isSecureConfig = determineIfSecureConfig();\n var secureLocation;\n\n if (!isSecureConfig) /* can't have a secure location without a secure configuration */\n return null;\n \n if (isDebugBuild || conn.url.protocol === 'https:' || conn.url.protocol === 'tcps:')\n secureLocation = true;\n else if (conn.isInitiator && conn._target.hasOwnProperty('friendLocation') && conn.url === conn._target.friendLocation)\n secureLocation = true;\n else if (conn.connectionOptions.allowUnencryptedSecrets)\n secureLocation = 'override';\n else\n secureLocation = false;\n\n debugging('strict-mode') && console.debug(`Location ${conn.url} is ${!secureLocation ? 'not secure' : 'secure-' + secureLocation}`);\n \n return secureLocation;\n}\n\nexports.Connection = Connection;\n\n\n//# sourceURL=webpack:///./src/protocol-v4/connection/connection.js?");
|
|
5449
5449
|
|
|
5450
5450
|
/***/ }),
|
|
5451
5451
|
|
|
@@ -5469,7 +5469,7 @@ eval("/** @file error-payload.js\n * @author Eddie Roosenmaallen <edd
|
|
|
5469
5469
|
/***/ (function(module, exports, __webpack_require__) {
|
|
5470
5470
|
|
|
5471
5471
|
"use strict";
|
|
5472
|
-
eval("/* WEBPACK VAR INJECTION */(function(Buffer) {/**\n * @file protocol/connection/identity-cache.js\n * @author Nazila Akhavan, Nazila@kingsds.network\n * @date January 2020\n *\n * The identity cache is used to ensure that for any connection baseHref and identity are match,\n * then insert them into the cache (browser/ node). It is also used to clear the cache. \n * \n * @todo: Consider replacing the node API calls with dcp-localstorage // RR March 2020\n */\n\n\nconst IDENTITY_CACHE_LOCAL_STORAGE_KEY = 'idCache';\nconst env = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\nconst { fs, requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\nconst { Address } = __webpack_require__(/*! dcp/dcp-client/wallet */ \"./src/dcp-client/wallet/index.js\");\n \nclass IdentityCache {\n constructor() {\n this.identities = { /* baseHref: address */};\n\n if (env.isBrowserPlatform) {\n try {\n this.identities = JSON.parse(window.localStorage.getItem(IDENTITY_CACHE_LOCAL_STORAGE_KEY)) || {};\n this.initIdentities();\n } catch (e) {\n console.warn(\"Failed to parse identity cache from local storage\", e);\n }\n } else if (fs.existsSync(this.cacheFilePath)) {\n let fd = fs.openSync(this.cacheFilePath)\n let stat = fs.fstatSync(fd);\n let buffer = Buffer.alloc(stat.size);\n fs.readSync(fd, buffer, 0, buffer.length, 0);\n fs.closeSync(fd);\n \n const cacheFile = buffer.toString('ascii');\n if (buffer.length) {\n this.identities = JSON.parse(cacheFile);\n this.initIdentities();\n }\n }\n }\n\n get cacheFolder() {\n return __webpack_require__(/*! dcp/common/dcp-dot-dir */ \"./src/common/dcp-dot-dir.js\").dotDcpDir;\n }\n\n get cacheFilePath() {\n return this.cacheFolder + '/identity-cache.json';\n }\n\n /**\n * Initialize Address instances for the identities after they are\n * retrieved from the cache.\n */\n initIdentities() {\n for (let baseHref in this.identities) {\n this.identities[baseHref] = new Address(this.identities[baseHref]);\n }\n }\n\n /**\n * Learn a new identity. In DCP, we always assume that the first identity we see for a given baseHref\n * is the authoritative one, unless contradicted at some point in the future by a presharedAddress. In\n * the case where the the psA is in conflict with what we already knew, we emit a log warning.\n *\n * @param {object} url a URL containing the base href (origin and pathname) associated \n * with this identity\n * @param {object} identity instance of eth::Address \n * @param {object} presharedAddress optional - address which is shared out-of-band (eg https) that we\n * *know* is safe for this resource and so can override our extant\n * knowledge\n *\n * @return false if what we have learned is in conflict with what's already in the cache (and not psA)\n * true if we were allowed to learn that this is the identity associated with\n * \n */\n learnIdentity (url, identity, presharedAddress)\n {\n const baseHref = url.origin + url.pathname;\n\n if (presharedAddress && identity.eq(presharedAddress))\n {\n if (!this.identities.hasOwnProperty(baseHref) || !identity.eq(this.identities[baseHref]))\n {\n console.warn(`**** Warning: preshared peer address ${presharedAddress} replaces ${this.identities[baseHref]} in identity cache ****`);\n this.identities[baseHref] = String(identity);\n }\n return true;\n }\n\n if (this.identities.hasOwnProperty(baseHref))\n return identity.eq(this.identities[baseHref]);\n\n this.identities[baseHref] = String(identity);\n return true;\n }\n\n /**\n * Return the identity associated with a given URL.\n *\n * @param {object} url a URL containing the base href (origin and pathname) for\n * the identity we seek\n */\n getIdentity(url) {\n const baseHref = url.origin + url.pathname;\n return this.identities[baseHref];\n }\n\n /** Save identities to the local storage or identity-cache.json */\n saveIdentities() {\n if (env.isBrowserPlatform) {\n window.localStorage.setItem('idCache', JSON.stringify(this.identities, null, 2));\n\n } else if (env.platform === 'nodejs') {\n try {\n if (!fs.existsSync(this.cacheFolder)){\n fs.mkdirSync(this.cacheFolder, { recursive: true });\n };\n\n fs.writeFileSync(this.cacheFilePath, JSON.stringify(this.identities));\n\n fs.chmod(this.cacheFilePath, 0o700, (err) => {\n if (err) throw err;\n });\n } catch (e) {\n // There are reasons mkdir might fail - such as not enough file handles. They shouldn't\n // be considered exceptional, and shouldn't cause an uncaught exception.\n console.error(e.message);\n }\n }\n }\n\n clearAll () {\n this.identities = { };\n this.saveIdentities();\n }\n\n clearIdentity (id) {\n for (let baseHref in this.identities) {\n if (this.identities[baseHref].eq(id)) {\n delete this.identities[baseHref];\n }\n }\n this.saveIdentities();\n }\n}\n\nlet singleton = null;\n\n/** @returns {IdentityCache} */\nexports.getGlobalIdentityCache = function() {\n if (!singleton) {\n singleton = new IdentityCache();\n }\n\n return singleton;\n}\n\n/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(/*! ./../../../node_modules/node-libs-browser/node_modules/buffer/index.js */ \"./node_modules/node-libs-browser/node_modules/buffer/index.js\").Buffer))\n\n//# sourceURL=webpack:///./src/protocol-v4/connection/identity-cache.js?");
|
|
5472
|
+
eval("/* WEBPACK VAR INJECTION */(function(Buffer) {/**\n * @file protocol/connection/identity-cache.js\n * @author Nazila Akhavan, Nazila@kingsds.network\n * @date January 2020\n *\n * The identity cache is used to ensure that for any connection baseHref and identity are match,\n * then insert them into the cache (browser/ node). It is also used to clear the cache. \n * \n * @todo: Consider replacing the node API calls with dcp-localstorage // RR March 2020\n */\n\n\nconst IDENTITY_CACHE_LOCAL_STORAGE_KEY = 'idCache';\nconst env = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\nconst { fs, requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\nconst { Address } = __webpack_require__(/*! dcp/dcp-client/wallet */ \"./src/dcp-client/wallet/index.js\");\n \nclass IdentityCache {\n constructor() {\n this.identities = { /* baseHref: address */};\n\n if (env.isBrowserPlatform) {\n try {\n this.identities = JSON.parse(window.localStorage.getItem(IDENTITY_CACHE_LOCAL_STORAGE_KEY)) || {};\n this.initIdentities();\n } catch (e) {\n console.warn(\"Failed to parse identity cache from local storage\", e);\n }\n } else if (fs.existsSync(this.cacheFilePath)) {\n let fd = fs.openSync(this.cacheFilePath)\n let stat = fs.fstatSync(fd);\n let buffer = Buffer.alloc(stat.size);\n fs.readSync(fd, buffer, 0, buffer.length, 0);\n fs.closeSync(fd);\n \n const cacheFile = buffer.toString('ascii');\n if (buffer.length) {\n this.identities = JSON.parse(cacheFile);\n this.initIdentities();\n }\n }\n }\n\n get cacheFolder() {\n return __webpack_require__(/*! dcp/common/dcp-dot-dir */ \"./src/common/dcp-dot-dir.js\").dotDcpDir;\n }\n\n get cacheFilePath() {\n return this.cacheFolder + '/identity-cache.json';\n }\n\n /**\n * Initialize Address instances for the identities after they are\n * retrieved from the cache.\n */\n initIdentities() {\n for (let baseHref in this.identities) {\n this.identities[baseHref] = new Address(this.identities[baseHref]);\n }\n }\n\n /**\n * Learn a new identity. In DCP, we always assume that the first identity we see for a given baseHref\n * is the authoritative one, unless contradicted at some point in the future by a presharedAddress. In\n * the case where the the psA is in conflict with what we already knew, we emit a log warning.\n *\n * @param {object} url a URL containing the base href (origin and pathname) associated \n * with this identity\n * @param {object} identity instance of eth::Address \n * @param {object} presharedAddress optional - address which is shared out-of-band (eg https) that we\n * *know* is safe for this resource and so can override our extant\n * knowledge\n *\n * @return false if what we have learned is in conflict with what's already in the cache (and not psA)\n * true if we were allowed to learn that this is the identity associated with\n * \n */\n learnIdentity (url, identity, presharedAddress)\n {\n const baseHref = url.origin + url.pathname;\n\n if (presharedAddress && identity.eq(presharedAddress))\n {\n if (!this.identities.hasOwnProperty(baseHref) || !identity.eq(this.identities[baseHref]))\n {\n if (this.identities[baseHref])\n console.warn(`**** Warning: preshared peer address ${presharedAddress} replaces ${this.identities[baseHref]} in identity cache ****`);\n this.identities[baseHref] = String(identity);\n }\n return true;\n }\n\n if (this.identities.hasOwnProperty(baseHref))\n return identity.eq(this.identities[baseHref]);\n\n this.identities[baseHref] = String(identity);\n return true;\n }\n\n /**\n * Return the identity associated with a given URL.\n *\n * @param {object} url a URL containing the base href (origin and pathname) for\n * the identity we seek\n */\n getIdentity(url) {\n const baseHref = url.origin + url.pathname;\n return this.identities[baseHref];\n }\n\n /** Save identities to the local storage or identity-cache.json */\n saveIdentities() {\n if (env.isBrowserPlatform) {\n window.localStorage.setItem('idCache', JSON.stringify(this.identities, null, 2));\n\n } else if (env.platform === 'nodejs') {\n try {\n if (!fs.existsSync(this.cacheFolder)){\n fs.mkdirSync(this.cacheFolder, { recursive: true });\n };\n\n fs.writeFileSync(this.cacheFilePath, JSON.stringify(this.identities));\n\n fs.chmod(this.cacheFilePath, 0o700, (err) => {\n if (err) throw err;\n });\n } catch (e) {\n // There are reasons mkdir might fail - such as not enough file handles. They shouldn't\n // be considered exceptional, and shouldn't cause an uncaught exception.\n console.error(e.message);\n }\n }\n }\n\n clearAll () {\n this.identities = { };\n this.saveIdentities();\n }\n\n clearIdentity (id) {\n for (let baseHref in this.identities) {\n if (this.identities[baseHref].eq(id)) {\n delete this.identities[baseHref];\n }\n }\n this.saveIdentities();\n }\n}\n\nlet singleton = null;\n\n/** @returns {IdentityCache} */\nexports.getGlobalIdentityCache = function() {\n if (!singleton) {\n singleton = new IdentityCache();\n }\n\n return singleton;\n}\n\n/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(/*! ./../../../node_modules/node-libs-browser/node_modules/buffer/index.js */ \"./node_modules/node-libs-browser/node_modules/buffer/index.js\").Buffer))\n\n//# sourceURL=webpack:///./src/protocol-v4/connection/identity-cache.js?");
|
|
5473
5473
|
|
|
5474
5474
|
/***/ }),
|
|
5475
5475
|
|
|
@@ -5505,7 +5505,7 @@ eval("/**\n * @file protocol/connection/message-factory.js\n * @author
|
|
|
5505
5505
|
/***/ (function(module, exports, __webpack_require__) {
|
|
5506
5506
|
|
|
5507
5507
|
"use strict";
|
|
5508
|
-
eval("/**\n * @file protocol/connection/message-ledger.js\n * @author Ryan Rossiter, ryan@kingsds.network\n * @date January 2020\n *\n * The MessageLedger is responsible for storing receipts for messages\n * that are given to the sender. The receipts are used to resolve the\n * promise that was returned from the sender, to indicate when the message\n * is complete.\n * \n * Request receipt promises are resolved once a response has been received.\n * Response receipt promises are resolved once the response has been sent.\n */\n\n\nconst debugging = __webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope('dcp');\nconst ONE_DAY_MS = 24 * 60 * 60 * 1000;\nclass MessageLedger {\n constructor(connection) {\n this.connection = connection;\n const id = this.connection._id;\n this.debugLabel = connection.isInitiator? `message-ledger(i#${id}):` : `message-ledger(t#${id}):`;\n this.receipts = new Map(); // id => {message, resolve, reject\n this.batches = new Map(); // id -> [...messages]\n this.silentFailMessage = false;\n }\n\n /**\n * This method is invoked by the sender upon sending a new request.\n * It stores a receipt of the message and returns a promise that will\n * be resolved when the message's lifecycle has completed.\n * \n * @param {Message} message - The message to keep a receipt of its eventual transmission \n */\n addMessage(message) {\n if (this.receipts.has(message.id)) {\n throw new
|
|
5508
|
+
eval("/**\n * @file protocol/connection/message-ledger.js\n * @author Ryan Rossiter, ryan@kingsds.network\n * @date January 2020\n *\n * The MessageLedger is responsible for storing receipts for messages\n * that are given to the sender. The receipts are used to resolve the\n * promise that was returned from the sender, to indicate when the message\n * is complete.\n * \n * Request receipt promises are resolved once a response has been received.\n * Response receipt promises are resolved once the response has been sent.\n */\n\n\nconst debugging = __webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope('dcp');\nconst ONE_DAY_MS = 24 * 60 * 60 * 1000;\nclass MessageLedger {\n constructor(connection) {\n this.connection = connection;\n const id = this.connection._id;\n this.debugLabel = connection.isInitiator? `message-ledger(i#${id}):` : `message-ledger(t#${id}):`;\n this.receipts = new Map(); // id => {message, resolve, reject\n this.batches = new Map(); // id -> [...messages]\n this.silentFailMessage = false;\n }\n\n /**\n * This method is invoked by the sender upon sending a new request.\n * It stores a receipt of the message and returns a promise that will\n * be resolved when the message's lifecycle has completed.\n * \n * @param {Message} message - The message to keep a receipt of its eventual transmission \n */\n addMessage(message) {\n if (this.receipts.has(message.id)) {\n throw new DCPError(`Duplicate transmission receipt ${JSON.stringify(message)}`, 'DCPC-1010');\n }\n this.ref();\n return new Promise((resolve, reject) => {\n this.receipts.set(message.id, { message, resolve, reject });\n }).catch((error) =>\n {\n if (!this.silentFailMessage)\n {\n throw error;\n }\n\n // If we \"silently fail\", we still need to resolve with the right-shaped\n // return value or things will break in hard-to-debug ways.\n return {\n success: false,\n payload: error,\n }\n });\n }\n\n /**\n * Analogous to timer/socket/server#ref. Calling ref 1 or more times means\n * the event loop will not exit until this is `unref`d.\n */\n ref() {\n if (this.refTimer) return;\n this.refTimer = setTimeout( () => {\n this.refTimer = null;\n this.ref();\n }, ONE_DAY_MS);\n }\n\n /**\n * Analogous to timer/socket/server#unref. Calling unref 1 or more times means\n * the event loop will not be prevented from exiting by this message-ledger.\n */\n unref() {\n clearTimeout(this.refTimer);\n this.refTimer = null;\n }\n\n /**\n * Add a batch to the ledger\n * @param {BatchMessage} batch A batch message which we are trying to send.\n */\n addBatch(batch) {\n this.batches.set(batch.id, batch);\n }\n\n /**\n * Remove a batch from the ledger now that its messages have been received.\n * Also mark the messages as sent.\n * @param {object} ack ack message \n */\n handleAck(ack) {\n const id = ack.messageId;\n if (this.receipts.has(id)) {\n this.handleReceiptAck(id, ack);\n } else if (this.batches.has(id)) {\n this.handleBatchAck(id, ack);\n } else {\n /* Do nothing, we don't mind spurious acks */\n debugging('message-ledger') && console.debug(this.debugLabel, 'received unhandled ack', ack.toJSON());\n }\n }\n\n /**\n * Find message in receipts and mark it as sent, also resolve/delete if its a response.\n * !Note: This is untested code since current implementation always batches before sending.\n * @param {string} id id of message that we have a receipt for\n * @param {object} ack ack message\n */\n handleReceiptAck(id, ack) {\n const receipt = this.receipts.get(id);\n const ackToken = receipt.message.ackToken || receipt.message.body.ackToken;\n if (ackToken === ack.token) {\n debugging('message-ledger') && console.debug(this.debugLabel, 'received receipt ack');\n this.fulfillResponsePromise(receipt.message);\n this.connection.sender.clearFlightDeck(receipt.message, ack.nonce);\n /* We don't delete a receipt until the promise can be fulfilled. */\n }\n }\n\n /**\n * Iterate over messages in batch and mark each as sent.\n * @param {string} id id of batch that we have sent\n * @param {object} ack ack message\n */\n handleBatchAck(id, ack) {\n const batch = this.batches.get(id);\n if (batch.ackToken === ack.token) {\n debugging('message-ledger') && console.debug(this.debugLabel, 'received batch ack');\n batch.messages.forEach( (m) => this.fulfillResponsePromise(m) );\n this.connection.sender.clearFlightDeck(batch, ack.nonce);\n this.batches.delete(id);\n } else {\n // we just ignore spurious acks\n debugging('message-ledger') && console.debug(this.debugLabel, 'received batch ack with wrong token', batch.ackToken, ack);\n }\n }\n /**\n * Like fulfillMessagePromise but ensures only responses are fulfilled.\n * @param {Connection.Message} message The response that we are fulfilling.\n */\n fulfillResponsePromise(message) {\n if (message instanceof this.connection.Response) {\n this.fulfillMessagePromise(message.id, true)\n }\n }\n\n /**\n * This method resolves\n * \n * @param {Message.id|string} id \n * @param {*|Error} value - Error instance to reject, resolves with value otherwise\n */\n fulfillMessagePromise(id, value) {\n if (this.receipts.has(id)) {\n let receipt = this.receipts.get(id);\n\n if (value instanceof Error && this.connection.rejectUnsentMessages)\n value = { success: false, error: value, info: 'reject-unsent' };\n\n if (value instanceof Error)\n receipt.reject(value);\n else\n receipt.resolve(value);\n\n this.receipts.delete(id);\n if (this.receipts.size === 0) this.unref();\n } else {\n /* Do nothing, we don't mind duplicate responses */\n debugging('message-ledger') && console.debug(this.debugLabel, 'received duplicate response', id);\n }\n }\n\n /**\n * Fail all pending transmission receipts with err.\n * \n * @param {Error} err \n */\n failAllTransmissions(err) {\n if (!(err instanceof Error)) {\n console.error(new Error(\"MessageLedger.failAllTransmissions: Argument must be instance of Error\"));\n err = new Error('invalid error: ' + err);\n }\n \n for (let id of this.receipts.keys()) {\n debugging('message-ledger') && console.debug('failing transmission:', id, '\\n ' + JSON.stringify(this.receipts.get(id)));\n this.fulfillMessagePromise(id, err);\n }\n }\n}\n\nObject.assign(module.exports, {\n MessageLedger,\n});\n\n\n//# sourceURL=webpack:///./src/protocol-v4/connection/message-ledger.js?");
|
|
5509
5509
|
|
|
5510
5510
|
/***/ }),
|
|
5511
5511
|
|
|
@@ -5527,7 +5527,7 @@ eval("/**\n * @file protocol/connection/message.js\n * @author Ryan
|
|
|
5527
5527
|
/*! no static exports found */
|
|
5528
5528
|
/***/ (function(module, exports, __webpack_require__) {
|
|
5529
5529
|
|
|
5530
|
-
eval("/**\n * @file protocol/connection/receiver.js\n * @author Ryan Rossiter, ryan@kingsds.network\n * @date January 2020\n *\n * The receiver class is responsible for listening on a transport instance\n * for messages, and for parsing and validating messages as they are received.\n * Upon a valid message, it will do the following:\n * On Request: emit on the 'request' event\n * On Response: resolve the request receipt that corresponds to the message ID\n */\n\nconst semver = __webpack_require__(/*! semver */ \"./node_modules/semver/semver.js\");\nconst DCP_ENV = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\n\nlet nanoid;\nlet perf;\nif (DCP_ENV.platform === 'nodejs') {\n const { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\n nanoid = requireNative('nanoid').nanoid;\n perf = requireNative('perf_hooks').performance;\n} else {\n nanoid = __webpack_require__(/*! nanoid */ \"./node_modules/nanoid/index.browser.js\").nanoid;\n perf = performance;\n}\n\nconst { EventEmitter } = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\");\nconst wallet = __webpack_require__(/*! dcp/dcp-client/wallet */ \"./src/dcp-client/wallet/index.js\");\nconst { ValidityStampCache } = __webpack_require__(/*! ./validity-stamp-cache */ \"./src/protocol-v4/connection/validity-stamp-cache.js\");\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\nconst debugging = __webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope('dcp');\n\nclass Receiver extends EventEmitter {\n constructor(connection, messageLedger) {\n super(\"Protocol Receiver\");\n\n this.connection = connection;\n this.messageLedger = messageLedger;\n const id = this.connection._id;\n this.debugLabel = this.connection.isInitiator? `receiver(i#${id}):` : `receiver(t#${id}):`;\n\n // Used by non-ntp connections to determine \"the present\"\n // time: seconds since epoch when response was sent\n // receivedMs: monotonically increasing timestamp of receipt in milliseconds\n this.lastResponseTiming = {\n time: Date.now() / 1000,\n receivedMs: perf.now(),\n }\n }\n\n /**\n * This method validates the ttl period on a request.\n * @param {Connection.Request} req\n * @returns {boolean} true if the ttl is valid, false otherwise\n */\n isTtlValid(req) {\n const validity = req.payload.validity;\n if (!validity || !validity.stamp || typeof validity.time !== 'number') {\n const err = new DCPError(\n 'Invalid request. Validity property must have `time` and `stamp`.',\n 'EINVAL',\n );\n req.respond(err);\n return false;\n } else {\n // determine ttl of the request\n let ttl;\n if (!validity.ttl || typeof validity.ttl !== 'number') {\n ttl = this.connection.ttl.default || this.connection.ttl.min;\n } else {\n if (validity > this.connection.ttl.max) ttl = this.connection.ttl.max;\n else if (validity.ttl < this.connection.ttl.min) ttl = this.connection.ttl.min;\n else ttl = validity.ttl;\n }\n\n const now = this.connection.currentTime();\n const minimumPresent = now - dcpConfig.dcp.validitySlopValue;\n const maximumPresent = now + dcpConfig.dcp.validitySlopValue;\n const expirationTime = validity.time + ttl;\n\n if (validity.time > maximumPresent) {\n const err = new DCPError(\n `Request was sent from the future (${validity.time} > ${maximumPresent})`,\n 'ETIMETRAVEL',\n );\n req.respond(err);\n return false;\n } else if (expirationTime < minimumPresent) {\n const err = new DCPError(\n `Request validity has expired (${expirationTime} < ${minimumPresent})`,\n 'EEXPIRED',\n {\n timestamp: validity.time,\n offset: now - validity.time,\n },\n );\n req.respond(err);\n return false;\n }\n\n try {\n ValidityStampCache.insert(validity.stamp, expirationTime);\n } catch (e) {\n const err = new DCPError(\n `Duplicate request validity stamp: ${validity.stamp}`,\n 'EDUP',\n );\n req.respond(err);\n return false;\n }\n }\n\n return true;\n }\n\n messageBodyDebugLogger(body, batchInfo)\n {\n const dcpsid = this.connection.dcpsid || '<new>';\n\n function now()\n {\n const d = new Date();\n var ms = (d % 1000) + '';\n var ds;\n\n switch (ms.length)\n {\n case 1:\n ms = '00' + ms;\n break;\n case 2:\n ms = '0' + ms;\n break;\n }\n \n ds = d.toLocaleTimeString().replace(/ .*/, '.' + ms);\n return ds;\n }\n \n if (body.type === 'response') {\n debugging() && console.debug(this.debugLabel, `${now()}: session ${dcpsid.slice(0,6)}${batchInfo} sent response`, (body.payload && body.payload.operation) || '');\n } else if (body.type === 'request') {\n debugging() && console.debug(this.debugLabel, `${now()}: session ${dcpsid.slice(0,6)}${batchInfo} sent request ${body.payload && body.payload.operation}`);\n } else if (body.type === 'batch') {\n debugging() && console.debug(this.debugLabel, `${now()}: session ${dcpsid.slice(0,6)}${batchInfo} sent batch of length ${body.payload.length}, ${body.id.slice(-6)}`);\n } else {\n debugging() && console.debug(this.debugLabel, `${now()}: session ${dcpsid.slice(0,6)}${batchInfo} sent invalid message body`, body);\n }\n }\n\n /**\n * Central implementation for dispatching events based on message bodies.\n */\n dispatchMessageBody(messageBody, owner, batchDebugLabel)\n {\n debugging('messages') && this.messageBodyDebugLogger(messageBody, batchDebugLabel);\n \n if (messageBody.type === 'response') {\n this.onResponse(messageBody, owner);\n } else if (messageBody.type === 'request') {\n this.onRequest(messageBody, owner);\n } else if (messageBody.type === 'batch') {\n this.onBatch(messageBody, owner);\n } else if (messageBody.type === 'unhandled-message') {\n debugging('connection') && console.warn('Ignoring unhandled message', JSON.stringify(messageBody));\n } else {\n throw new DCPError(`Message in batch has unknown type: ${messageBody.type}`, 'EINTERNAL');\n }\n }\n \n /**\n * Central method for receiver to begin processing a message.\n * This method trusts that the message has a valid dcpsid, signature,\n * and nonce.\n * @param {object} message validated message object\n */\n onMessage(message) {\n const owner = new wallet.Address(message.owner);\n\n this.dispatchMessageBody(message.body, owner, '');\n }\n\n /**\n * This method is invoked when the connection receives a new request.\n * It should validate the request against the session state, and handle\n * special-case session operations like 'connect' and 'close'.\n *\n * @param {Object} msgBody\n * @param {wallet.Address} owner\n */\n onRequest(msgBody, owner) {\n const req = this.connection.Request.fromJSON(msgBody, owner);\n\n if (this.connection.state.isNot('established')) { /* XXXwg narrow down this list */\n if (req.payload.operation !== 'connect') {\n const message = \"First request operation received from peer was not 'connect'\";\n debugging('connection') && console.error(this.debugLabel, message);\n this.connection.close(message, true);\n return\n }\n this.handleFirstRequest(req).catch( (err) => {\n console.error(this.debugLabel, `bad first request was sent to target. ${err.code || '(no code)'}`, err, err.code);\n this.connection.close(err, true);\n });\n } else if (!this.isTtlValid(req)) {\n return;\n } else {\n let reqId = req.id;\n \n this.connection.openRequests[reqId] = msgBody;\n setTimeout(() => {\n /* req.id frequently mutates between memoization and timeout */\n delete this.connection.openRequests[reqId];\n }, 1000);\n this.handleOperation(req);\n }\n }\n\n async handleFirstRequest(req) {\n const cacheSize = 10; /* 0 should be enough */\n var cache; /* we keep a cache of recently-seen half sid in case sender retransmits a connect message */\n var bottomHalfSid;\n \n debugging('connection') && console.debug(this.debugLabel, 'received first request.');\n if (!this.handleFirstRequest.cache)\n this.handleFirstRequest.cache = {};\n cache = this.handleFirstRequest.cache;\n \n const initiatorVersion = req.payload.data.version;\n const targetCompatibility = this.connection.constructor.VERSION_COMPATIBILITY;\n const versionCompatible = semver.satisfies(initiatorVersion, targetCompatibility);\n if (!versionCompatible) {\n const versionErr = new DCPError(targetCompatibility, 'EVERSION');\n const connectResponse = new this.connection.Response(req, versionErr);\n await this.connection.sender.specialFirstSend(connectResponse);\n throw versionErr;\n }\n\n const topHalfSid = req.payload.data.sid;\n\n if (cache.hasOwnProperty(topHalfSid)) {\n bottomHalfSid = cache[topHalfSid];\n console.error(`*** Warning: remote presented recently-seen half sid ${topHalfSid}; reusing ${bottomHalfSid}`);\n } else {\n bottomHalfSid = nanoid();\n cache[topHalfSid] = bottomHalfSid;\n if (Object.keys(cache).length > cacheSize) {\n for (let prop in cache) {\n delete cache[prop];\n break;\n }\n }\n }\n\n if (typeof topHalfSid !== 'string' || topHalfSid.length < bottomHalfSid.length) {\n throw new DCPError(`Initiator sent an invalid DCPSID: ${topHalfSid}`, 'EDCPSID');\n }\n\n // Append our half of the dcpsid\n const dcpsid = topHalfSid + bottomHalfSid;\n const connectResponse = new this.connection.Response(req, {\n version: this.connection.constructor.VERSION,\n operation: 'connect'\n });\n\n // we must emit `established` before sending this response to ensure\n // that when the initiator sends its first request we are in that state.\n this.connection.establishTarget(dcpsid, req.owner);\n try {\n await this.connection.sender.specialFirstSend(connectResponse);\n } catch (error) {\n console.error('Failed to send `connect` response.');\n return;\n }\n\n // usually established, but it's possible to close before this fires.\n debugging('connection') && console.debug(\n this.debugLabel, \n 'first request handled. Connection state:', \n this.connection.state.valueOf()\n );\n }\n\n handleOperation(req) {\n switch (req.payload.operation) {\n case 'connect':\n throw new DCPError('Request operation \"connect\" received after establishment', 'ETOOMANYCONNECT');\n case 'close':\n this.connection.closeWait(req.payload.errorCode).catch(error => console.error('Unexpected error closing initiator:', error.message));\n break;\n \n case 'keepalive':\n req.respond();\n break;\n\n default:\n this.emit('request', req);\n break;\n }\n }\n\n /**\n * This method is invoked when the connection receives a new response.\n * It should validate the response against the session state.\n *\n * @param {Object} msgBody\n * @param {wallet.Address} owner\n */\n onResponse(msgBody, owner) {\n debugging('connection') && console.debug(this.debugLabel, 'received response.');\n const resp = this.connection.Response.fromJSON(msgBody, owner);\n\n this.lastResponseTiming.time = resp.time;\n this.lastResponseTiming.receivedMs = perf.now();\n\n this.messageLedger.fulfillMessagePromise(resp.id, resp);\n }\n\n /**\n * This method handles rehydrating Batch Requests and then\n * passes internal messages on to appropriate handlers.\n * @param {Object} msgBody\n * @param {wallet.Address} owner\n */\n onBatch(msgBody, owner) {\n const batch = this.connection.Batch.fromJSON(msgBody, owner);\n var batchDebugLabel;\n \n debugging() && (batchDebugLabel = ' batch ' + batch.id.slice(-6));\n\n for (let messageBody of batch.messageObjects)\n this.dispatchMessageBody(messageBody, owner, batchDebugLabel);\n }\n}\n\nObject.assign(module.exports, {\n Receiver,\n});\n\n\n//# sourceURL=webpack:///./src/protocol-v4/connection/receiver.js?");
|
|
5530
|
+
eval("/**\n * @file protocol/connection/receiver.js\n * @author Ryan Rossiter, ryan@kingsds.network\n * @date January 2020\n *\n * The receiver class is responsible for listening on a transport instance\n * for messages, and for parsing and validating messages as they are received.\n * Upon a valid message, it will do the following:\n * On Request: emit on the 'request' event\n * On Response: resolve the request receipt that corresponds to the message ID\n */\n\nconst semver = __webpack_require__(/*! semver */ \"./node_modules/semver/semver.js\");\nconst DCP_ENV = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\n\nlet nanoid;\nlet perf;\nif (DCP_ENV.platform === 'nodejs') {\n const { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\n nanoid = requireNative('nanoid').nanoid;\n perf = requireNative('perf_hooks').performance;\n} else {\n nanoid = __webpack_require__(/*! nanoid */ \"./node_modules/nanoid/index.browser.js\").nanoid;\n perf = performance;\n}\n\nconst { EventEmitter } = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\");\nconst wallet = __webpack_require__(/*! dcp/dcp-client/wallet */ \"./src/dcp-client/wallet/index.js\");\nconst { ValidityStampCache } = __webpack_require__(/*! ./validity-stamp-cache */ \"./src/protocol-v4/connection/validity-stamp-cache.js\");\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\nconst debugging = __webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope('dcp');\n\nclass Receiver extends EventEmitter {\n constructor(connection, messageLedger) {\n super(\"Protocol Receiver\");\n\n this.connection = connection;\n this.messageLedger = messageLedger;\n const id = this.connection._id;\n this.debugLabel = this.connection.isInitiator? `receiver(i#${id}):` : `receiver(t#${id}):`;\n\n // Used by non-ntp connections to determine \"the present\"\n // time: seconds since epoch when response was sent\n // receivedMs: monotonically increasing timestamp of receipt in milliseconds\n this.lastResponseTiming = {\n time: Date.now() / 1000,\n receivedMs: perf.now(),\n }\n }\n\n /**\n * This method validates the ttl period on a request.\n * @param {Connection.Request} req\n * @returns {boolean} true if the ttl is valid, false otherwise\n */\n isTtlValid(req) {\n const validity = req.payload.validity;\n if (!validity || !validity.stamp || typeof validity.time !== 'number') {\n const err = new DCPError(\n 'Invalid request. Validity property must have `time` and `stamp`.',\n 'EINVAL',\n );\n req.respond(err);\n return false;\n } else {\n // determine ttl of the request\n let ttl;\n if (!validity.ttl || typeof validity.ttl !== 'number') {\n ttl = this.connection.ttl.default || this.connection.ttl.min;\n } else {\n if (validity > this.connection.ttl.max) ttl = this.connection.ttl.max;\n else if (validity.ttl < this.connection.ttl.min) ttl = this.connection.ttl.min;\n else ttl = validity.ttl;\n }\n\n const now = this.connection.currentTime();\n const minimumPresent = now - dcpConfig.dcp.validitySlopValue;\n const maximumPresent = now + dcpConfig.dcp.validitySlopValue;\n const expirationTime = validity.time + ttl;\n\n if (validity.time > maximumPresent) {\n const err = new DCPError(\n `Request was sent from the future (${validity.time} > ${maximumPresent})`,\n 'ETIMETRAVEL',\n );\n req.respond(err);\n return false;\n } else if (expirationTime < minimumPresent) {\n const err = new DCPError(\n `Request validity has expired (${expirationTime} < ${minimumPresent})`,\n 'EEXPIRED',\n {\n timestamp: validity.time,\n offset: now - validity.time,\n },\n );\n req.respond(err);\n return false;\n }\n\n try {\n ValidityStampCache.insert(validity.stamp, expirationTime);\n } catch (e) {\n const err = new DCPError(\n `Duplicate request validity stamp: ${validity.stamp}`,\n 'EDUP',\n );\n req.respond(err);\n return false;\n }\n }\n\n return true;\n }\n\n messageBodyDebugLogger(body, batchInfo)\n {\n const dcpsid = this.connection.dcpsid || '<new>';\n\n function now()\n {\n const d = new Date();\n var ms = (d % 1000) + '';\n var ds;\n\n switch (ms.length)\n {\n case 1:\n ms = '00' + ms;\n break;\n case 2:\n ms = '0' + ms;\n break;\n }\n \n ds = d.toLocaleTimeString().replace(/ .*/, '.' + ms);\n return ds;\n }\n \n if (body.type === 'response') {\n debugging() && console.debug(this.debugLabel, `${now()}: session ${dcpsid.slice(0,6)}${batchInfo} sent response`, (body.payload && body.payload.operation) || '');\n } else if (body.type === 'request') {\n debugging() && console.debug(this.debugLabel, `${now()}: session ${dcpsid.slice(0,6)}${batchInfo} sent request ${body.payload && body.payload.operation}`);\n } else if (body.type === 'batch') {\n debugging() && console.debug(this.debugLabel, `${now()}: session ${dcpsid.slice(0,6)}${batchInfo} sent batch of length ${body.payload.length}, ${body.id.slice(-6)}`);\n } else {\n debugging() && console.debug(this.debugLabel, `${now()}: session ${dcpsid.slice(0,6)}${batchInfo} sent invalid message body`, body);\n }\n }\n\n /**\n * Central implementation for dispatching events based on message bodies.\n */\n dispatchMessageBody(messageBody, owner, batchDebugLabel)\n {\n debugging('messages') && this.messageBodyDebugLogger(messageBody, batchDebugLabel);\n \n if (messageBody.type === 'response') {\n this.onResponse(messageBody, owner);\n } else if (messageBody.type === 'request') {\n this.onRequest(messageBody, owner);\n } else if (messageBody.type === 'batch') {\n this.onBatch(messageBody, owner);\n } else if (messageBody.type === 'unhandled-message') {\n debugging('connection') && console.warn('Ignoring unhandled message', JSON.stringify(messageBody));\n } else {\n throw new DCPError(`Message in batch has unknown type: ${messageBody.type}`, 'DCPC-1009');\n }\n }\n \n /**\n * Central method for receiver to begin processing a message.\n * This method trusts that the message has a valid dcpsid, signature,\n * and nonce.\n * @param {object} message validated message object\n */\n handleMessage(message) {\n const owner = new wallet.Address(message.owner);\n\n this.dispatchMessageBody(message.body, owner, '');\n }\n\n /**\n * This method is invoked when the connection receives a new request.\n * It should validate the request against the session state, and handle\n * special-case session operations like 'connect' and 'close'.\n *\n * @param {Object} msgBody\n * @param {wallet.Address} owner\n */\n onRequest(msgBody, owner) {\n const req = this.connection.Request.fromJSON(msgBody, owner);\n\n if (this.connection.state.isNot('established')) { /* XXXwg narrow down this list */\n if (req.payload.operation !== 'connect') {\n const message = \"First request operation received from peer was not 'connect'\";\n debugging('connection') && console.error(this.debugLabel, message);\n this.connection.close(message, true);\n return\n }\n this.handleFirstRequest(req).catch( (err) => {\n console.error(this.debugLabel, `bad first request was sent to target. ${err.code || '(no code)'}`, err, err.code);\n this.connection.close(err, true);\n });\n } else if (!this.isTtlValid(req)) {\n return;\n } else {\n let reqId = req.id;\n \n this.connection.openRequests[reqId] = msgBody;\n setTimeout(() => {\n /* req.id frequently mutates between memoization and timeout */\n delete this.connection.openRequests[reqId];\n }, 1000);\n this.handleOperation(req);\n }\n }\n\n async handleFirstRequest(req) {\n const cacheSize = 10; /* 0 should be enough */\n var cache; /* we keep a cache of recently-seen half sid in case sender retransmits a connect message */\n var bottomHalfSid;\n \n debugging('connection') && console.debug(this.debugLabel, 'received first request.');\n if (!this.handleFirstRequest.cache)\n this.handleFirstRequest.cache = {};\n cache = this.handleFirstRequest.cache;\n \n const initiatorVersion = req.payload.data.version;\n const targetCompatibility = this.connection.constructor.VERSION_COMPATIBILITY;\n const versionCompatible = semver.satisfies(initiatorVersion, targetCompatibility);\n if (!versionCompatible) {\n const versionErr = new DCPError(targetCompatibility, 'DCPC-EVERSION');\n const connectResponse = new this.connection.Response(req, versionErr);\n await this.connection.sender.specialFirstSend(connectResponse);\n throw versionErr;\n }\n\n const topHalfSid = req.payload.data.sid;\n\n if (cache.hasOwnProperty(topHalfSid)) {\n bottomHalfSid = cache[topHalfSid];\n console.error(`*** Warning: remote presented recently-seen half sid ${topHalfSid}; reusing ${bottomHalfSid}`);\n } else {\n bottomHalfSid = nanoid();\n cache[topHalfSid] = bottomHalfSid;\n if (Object.keys(cache).length > cacheSize) {\n for (let prop in cache) {\n delete cache[prop];\n break;\n }\n }\n }\n\n if (typeof topHalfSid !== 'string' || topHalfSid.length < bottomHalfSid.length) {\n throw new DCPError(`Initiator sent an invalid DCPSID: ${topHalfSid}`, 'DCPC-EDCPSID');\n }\n\n // Append our half of the dcpsid\n const dcpsid = topHalfSid + bottomHalfSid;\n const connectResponse = new this.connection.Response(req, {\n version: this.connection.constructor.VERSION,\n operation: 'connect'\n });\n\n // we must emit `established` before sending this response to ensure\n // that when the initiator sends its first request we are in that state.\n this.connection.establishTarget(dcpsid, req.owner);\n try {\n await this.connection.sender.specialFirstSend(connectResponse);\n } catch (error) {\n console.error('Failed to send `connect` response.');\n return;\n }\n\n // usually established, but it's possible to close before this fires.\n debugging('connection') && console.debug(\n this.debugLabel, \n 'first request handled. Connection state:', \n this.connection.state.valueOf()\n );\n }\n\n handleOperation(req) {\n switch (req.payload.operation) {\n case 'connect':\n throw new DCPError('Request operation \"connect\" received after establishment', 'DCPC-ETOOMANYCONNECT');\n case 'close':\n this.connection.closeWait(req.payload.errorCode).catch(error => console.error('Unexpected error closing initiator:', error.message));\n break;\n \n case 'keepalive':\n req.respond();\n break;\n\n default:\n this.emit('request', req);\n break;\n }\n }\n\n /**\n * This method is invoked when the connection receives a new response.\n * It should validate the response against the session state.\n *\n * @param {Object} msgBody\n * @param {wallet.Address} owner\n */\n onResponse(msgBody, owner) {\n debugging('connection') && console.debug(this.debugLabel, 'received response.');\n const resp = this.connection.Response.fromJSON(msgBody, owner);\n\n this.lastResponseTiming.time = resp.time;\n this.lastResponseTiming.receivedMs = perf.now();\n\n this.messageLedger.fulfillMessagePromise(resp.id, resp);\n }\n\n /**\n * This method handles rehydrating Batch Requests and then\n * passes internal messages on to appropriate handlers.\n * @param {Object} msgBody\n * @param {wallet.Address} owner\n */\n onBatch(msgBody, owner) {\n const batch = this.connection.Batch.fromJSON(msgBody, owner);\n var batchDebugLabel;\n \n debugging() && (batchDebugLabel = ' batch ' + batch.id.slice(-6));\n\n for (let messageBody of batch.messageObjects)\n this.dispatchMessageBody(messageBody, owner, batchDebugLabel);\n }\n}\n\nObject.assign(module.exports, {\n Receiver,\n});\n\n\n//# sourceURL=webpack:///./src/protocol-v4/connection/receiver.js?");
|
|
5531
5531
|
|
|
5532
5532
|
/***/ }),
|
|
5533
5533
|
|
|
@@ -5563,7 +5563,7 @@ eval("/**\n * @file protocol/connection/response.js\n * @author Ryan
|
|
|
5563
5563
|
/***/ (function(module, exports, __webpack_require__) {
|
|
5564
5564
|
|
|
5565
5565
|
"use strict";
|
|
5566
|
-
eval("/* WEBPACK VAR INJECTION */(function(setImmediate) {/**\n * @file protocol/connection/sender.js\n * @author Ryan Rossiter, ryan@kingsds.network\n * @date January 2020\n *\n * The sender class is responsible for accepting Connection.Message instances,\n * and sending them to the peer via the provided transport instance.\n * Messages are queued in an array, and are sent in FIFO order - with the exception\n * of requests being skipped until there is a nonce available to send them with.\n */\n\n\nconst semver = __webpack_require__(/*! semver */ \"./node_modules/semver/semver.js\");\nconst DCP_ENV = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\nconst { assert } = __webpack_require__(/*! dcp/common/dcp-assert */ \"./src/common/dcp-assert.js\");\nconst debugging = __webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope('dcp');\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\n\nlet nanoid;\nif (DCP_ENV.platform === 'nodejs') {\n const { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\n nanoid = requireNative('nanoid').nanoid;\n} else {\n nanoid = __webpack_require__(/*! nanoid */ \"./node_modules/nanoid/index.browser.js\").nanoid;\n}\n\nclass Sender {\n constructor(connection) {\n this.connection = connection;\n this.messageLedger = this.connection.messageLedger;\n\n this.queue = [];\n this.inFlight = null;\n this.nonce = null;\n this._ackTokenId = 0;\n const id = this.connection._id;\n this.debugLabel = this.connection.isInitiator? `sender(i#${id}):` : `sender(t#${id}):`;\n\n // Always re-engage sending machinery once connection is re-established\n // further: reset send props for queued/inFlight messages\n // since re-establishment resets timeouts, send count, etc.\n this.connection.state.on('change', (newState, oldState) => {\n if (oldState === 'waiting' && newState === 'established') {\n this.queue.forEach( m => m.initSendProps());\n \n if (this.inFlight) {\n this.inFlight.message.initSendProps();\n this.doSend();\n } else {\n this.requestQueueService();\n }\n }\n })\n }\n\n /**\n * We generate a unique token with each message we send so that \n * the peer can quickly ack the message and prove identity/uniqueness\n */\n makeAckToken() {\n return `${this.connection._id}-${this._ackTokenId++}-${nanoid()}`;\n }\n\n /**\n * Initiates the session by sending the 'connect' operation.\n * Once the response has been received, the session is established.\n * Lots of one-off code in here since it's difficult to re-use the \n * same methods meant for normal messages.\n * \n * @returns {Object} Session info\n */\n async establish() {\n assert(this.connection.transport);\n const halfSid = nanoid();\n const initiatorVersion = this.connection.constructor.VERSION;\n const connectRequest = new this.connection.Request('connect', {\n version: initiatorVersion,\n sid: halfSid,\n });\n connectRequest.id = `${this.connection._id}-connect-${nanoid()}`;\n debugging('connection') && console.debug(this.debugLabel, 'sending initial connect message');\n const resp = await this.specialFirstSend(connectRequest);\n\n if (resp.payload instanceof this.connection.ErrorPayload) {\n assert(resp.payload.type === 'protocol');\n throw new DCPError(resp.payload.message, resp.payload.code);\n }\n const targetVersion = resp.payload.version;\n const initiatorCompatibility = this.connection.constructor.VERSION_COMPATIBILITY;\n const versionCompatible = semver.satisfies(targetVersion, initiatorCompatibility);\n if (!versionCompatible) {\n throw new DCPError(`Target's version (${targetVersion}) is not compatible (must meet ${initiatorCompatibility})`, 'ETARGETVERSION');\n }\n\n // verify that our dcpsid half is still there, followed by a string\n // provided by the target that is at least the same length.\n // escape special chars:\n const regexSafeSid = halfSid.replace(/[-\\/\\\\^$*+?.()|[\\\\]{}]/g, '\\\\$&');\n const sidRegex = new RegExp(`^${regexSafeSid}.{${halfSid.length},}$`);\n if (typeof resp.dcpsid !== 'string' || !resp.dcpsid.match(sidRegex)) {\n throw new Error(`Target responded with invalid DCPSID: ${resp.dcpsid}`);\n }\n debugging('connection') && console.debug(this.debugLabel, 'connection established.');\n return {\n dcpsid: resp.dcpsid,\n peerAddress: resp.owner,\n };\n }\n \n /**\n * Perform any remaining `establish` tasks\n * now that connection is fully ready.\n */\n finishSetup() {\n this.requestQueueService();\n }\n\n /**\n * We cannot use the normal enqueue logic for the first message\n * and we need many of the same bits of logic (but not all) so\n * this is the place for that kind of one-off logic.\n * @param {Message} message \n */\n async specialFirstSend(message) { /* XXXwg - special first send *will* cause double dcpsid if invoked twice */\n message.initSendProps();\n const messageSentPromise = this.messageLedger.addMessage(message);\n await this.createAndSend([message]); /* XXXwg - messageLedger may leak messages when createAndSend throws */\n if (message instanceof this.connection.Response) {\n this.connection.registerConnectResponse(this.inFlight.message); /* XXXwg todo - audit this.inFlight */\n }\n return messageSentPromise;\n }\n\n /**\n * Places message into `queue` but only schedules queue to be serviced\n * if connection is ready to send. Otherwise other tools will have to\n * handle making it ready to send again.\n * @param {Connection.Request|Connection.Response} message\n * @returns {Promise} from messageLedger\n * @resolves to a response if sending a request, or resolves when the response was sent.\n */\n enqueue(message) {\n /* Note - it looks like message.type is always undefined in here, but that somehow \n * the message is mutated between enqueue and transmission. /wg Nov 2021\n */\n if (message.payload)\n debugging('connection') && console.debug(this.debugLabel, `enqueueing ${message.type} message for operation:`, message.payload && message.payload.operation);\n else\n debugging('connection') && console.debug(this.debugLabel, `enqueueing ${message.type} message`);\n \n message.initSendProps();\n this.queue.push(message);\n this.requestQueueService();\n return this.messageLedger.addMessage(message);\n }\n\n /**\n * Kick off the send loops (outer and inner) on the next event cycle.\n * This is a 'request' because if the loop was already going to run\n * anyways this method has no effect.\n */\n requestQueueService () {\n if (this.requestQueueServiceTimeout) return;\n this.requestQueueServiceTimeout = setTimeout( () => {\n this.requestQueueServiceTimeout = null;\n this.serviceQueue();\n });\n }\n\n /**\n * Creates a batch and puts it in flight if the connection is established \n * and no batch is already in flight.\n */\n async serviceQueue () {\n if (!this.inFlight && this.connection.state.in(['established', 'closing', 'close-wait'])) {\n await this.createAndSend();\n } else {\n debugging('connection') && console.debug(this.debugLabel, `ignoring call to service queue. inFlight=${!!this.inFlight} state=${this.connection.state.valueOf()}`);\n }\n }\n\n /**\n * Creates a batch if it can find any messages to send and sends them.\n * If there are no messages to send, simply returns.\n * @param {Array<Connection.Message>} [messages] Array of messages to batch together for a transmission. \n * if not provided, draws from queue.\n */\n async createAndSend(messages=[]) {\n if (messages.length === 0) {\n messages = this.queue.splice(0, this.batchSize);\n debugging('connection') && console.debug(\n this.debugLabel, \n `pulled ${messages.length} message${messages.length === 1? '' : 's'} from queue.`\n );\n }\n if (messages.length === 0) return;\n assert(!this.inFlight);\n \n let op;\n let opIsReq = false;\n if (messages.length === 1) { // ie. no need to batch, can send single message\n op = messages[0];\n op.ackToken = this.makeAckToken();\n opIsReq = true;\n } else {\n op = new this.connection.Batch(messages, this.makeAckToken());\n }\n\n op.nonce = this.nonce;\n op.initSendProps();\n let signedOp = await op.sign();\n this.inFlight = { message: op, signedMessage: signedOp };\n if (!opIsReq)\n this.messageLedger.addBatch(op);\n // user can modify what is in flight, so we hop off the event loop /* XXXwg very dangerous - check spec */\n this.connection.emit('send', this.inFlight);\n await new Promise(r => setImmediate(r));\n // Connection may start shutting down while awaiting and sets this.inFlight=null\n if(!this.inFlight) return;\n // now we're back to sending\n debugging('connection') && console.debug(this.debugLabel, `sending a signed batch/request of ${messages.length}`);\n this.doSend();\n }\n \n /**\n * Sends the message stored in the `inFlight` var over the transport.\n * Schedules a future time to send it again.\n * `clearFlightDeck` is the only method that should be clearing this timer\n * and thus closing the loop.\n */\n doSend() {\n assert(this.inFlight);\n let op = this.inFlight.message;\n let signedOp = this.inFlight.signedMessage;\n try {\n op.updateSendProps();\n if (op instanceof this.connection.Request || op instanceof this.connection.Response) {\n debugging('connection') && console.debug(this.debugLabel, 'sending request/response. Next send in (ms):', op.msUntilNextSend);\n this.retryLoopTimeout = setTimeout(this.sendReqConditionally.bind(this), op.msUntilNextSend)\n } else if (op instanceof this.connection.Batch) {\n debugging('connection') && console.debug(this.debugLabel, 'sending batch. Next send in (ms):', op.msUntilNextSend);\n this.retryLoopTimeout = setTimeout(this.sendBatchConditionally.bind(this), op.msUntilNextSend);\n }\n this.connection.transport.send(signedOp);\n } catch (error) {\n console.error(`Error while signing and sending message to ${this.connection.url}:`, error);\n }\n }\n \n /**\n * Calls `doSend` if the request we are sending has not exceeded its limits\n */\n sendReqConditionally() {\n if (!this.inFlight)\n return\n let req = this.inFlight.message;\n if (req.overSendLimits) {\n debugging('connection') && console.debug(this.debugLabel, `Failing transport; reason: ${req.failReason}`);\n this.connection.failTransport(`Failing transport; reason: ${req.failReason}`); \n } else {\n this.doSend();\n }\n }\n\n /**\n * Calls `doSend` if the batch we are sending has not exceeded its limits\n */\n sendBatchConditionally() {\n // Prevent \"'message' of null\" error\n if (!this.inFlight)\n return;\n let batch = this.inFlight.message;\n if (batch.overSendLimits || (batch.messages[0].payload && batch.messages[0].payload.operation === 'connect')) {\n debugging('connection') && console.debug(this.debugLabel, `Failing transport; reason: ${batch.failReason}`);\n this.connection.failTransport(`Failing transport; reason: ${batch.failReason}`);\n } else {\n this.doSend();\n }\n }\n\n /**\n * Clear a message from the flight deck. For the foreseeable future the deck\n * only holds one message at a time, so we just assert that it matches.\n * @param {Connection.Message} message message that can be cleared from the flight deck\n */\n clearFlightDeck(message, nonce) {\n debugging('connection') && console.debug(this.debugLabel, 'clearing flight deck. nonce =', nonce);\n assert(message === this.inFlight.message);\n this.inFlight = null;\n this.nonce = nonce;\n clearTimeout(this.retryLoopTimeout);\n this.requestQueueService();\n }\n\n /**\n * When a connection is closed the sender needs to cancel its send efforts.\n */\n shutdown() {\n debugging('connection') && console.debug(this.debugLabel, 'shutting down.');\n this.inFlight = null;\n this.nonce = null;\n this.queue = [];\n clearTimeout(this.requestQueueServiceTimeout);\n clearTimeout(this.retryLoopTimeout);\n }\n\n // When allowBatch=false, set the batchSize to 1 so each\n // message gets sent individually (not in a batch)\n get batchSize() {\n if (this._batchSize) return this._batchSize;\n const batchSize = Math.max(this.connection.connectionOptions.maxMessagesPerBatch, 1);\n return this._batchSize = this.connection.connectionOptions.allowBatch ? batchSize : 1;\n }\n}\n\nObject.assign(module.exports, {\n Sender,\n});\n\n/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(/*! ./../../../node_modules/timers-browserify/main.js */ \"./node_modules/timers-browserify/main.js\").setImmediate))\n\n//# sourceURL=webpack:///./src/protocol-v4/connection/sender.js?");
|
|
5566
|
+
eval("/* WEBPACK VAR INJECTION */(function(setImmediate) {/**\n * @file protocol/connection/sender.js\n * @author Ryan Rossiter, ryan@kingsds.network\n * @date January 2020\n *\n * The sender class is responsible for accepting Connection.Message instances,\n * and sending them to the peer via the provided transport instance.\n * Messages are queued in an array, and are sent in FIFO order - with the exception\n * of requests being skipped until there is a nonce available to send them with.\n */\n\n\nconst semver = __webpack_require__(/*! semver */ \"./node_modules/semver/semver.js\");\nconst DCP_ENV = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\nconst { assert } = __webpack_require__(/*! dcp/common/dcp-assert */ \"./src/common/dcp-assert.js\");\nconst debugging = __webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope('dcp');\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\n\nlet nanoid;\nif (DCP_ENV.platform === 'nodejs') {\n const { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\n nanoid = requireNative('nanoid').nanoid;\n} else {\n nanoid = __webpack_require__(/*! nanoid */ \"./node_modules/nanoid/index.browser.js\").nanoid;\n}\n\nclass Sender {\n constructor(connection) {\n this.connection = connection;\n this.messageLedger = this.connection.messageLedger;\n\n this.queue = [];\n this.inFlight = null;\n this.nonce = null;\n this._ackTokenId = 0;\n const id = this.connection._id;\n this.debugLabel = this.connection.isInitiator? `sender(i#${id}):` : `sender(t#${id}):`;\n\n // Always re-engage sending machinery once connection is re-established\n // further: reset send props for queued/inFlight messages\n // since re-establishment resets timeouts, send count, etc.\n this.connection.state.on('change', (newState, oldState) => {\n if (oldState === 'waiting' && newState === 'established') {\n this.queue.forEach( m => m.initSendProps());\n \n if (this.inFlight) {\n this.inFlight.message.initSendProps();\n this.doSend();\n } else {\n this.requestQueueService();\n }\n }\n })\n }\n\n /**\n * We generate a unique token with each message we send so that \n * the peer can quickly ack the message and prove identity/uniqueness\n */\n makeAckToken() {\n return `${this.connection._id}-${this._ackTokenId++}-${nanoid()}`;\n }\n\n /**\n * Initiates the session by sending the 'connect' operation.\n * Once the response has been received, the session is established.\n * Lots of one-off code in here since it's difficult to re-use the \n * same methods meant for normal messages.\n * \n * @returns {Object} Session info\n */\n async establish() {\n assert(this.connection.transport);\n const halfSid = nanoid();\n const initiatorVersion = this.connection.constructor.VERSION;\n const connectRequest = new this.connection.Request('connect', {\n version: initiatorVersion,\n sid: halfSid,\n });\n connectRequest.id = `${this.connection._id}-connect-${nanoid()}`;\n debugging('connection') && console.debug(this.debugLabel, 'sending initial connect message');\n const resp = await this.specialFirstSend(connectRequest);\n\n if (resp.payload instanceof this.connection.ErrorPayload) {\n assert(resp.payload.type === 'protocol');\n throw new DCPError(resp.payload.message, resp.payload.code);\n }\n const targetVersion = resp.payload.version;\n const initiatorCompatibility = this.connection.constructor.VERSION_COMPATIBILITY;\n const versionCompatible = semver.satisfies(targetVersion, initiatorCompatibility);\n if (!versionCompatible) {\n throw new DCPError(`Target's version (${targetVersion}) is not compatible (must meet ${initiatorCompatibility})`, 'DCPC-ETARGETVERSION');\n }\n\n // verify that our dcpsid half is still there, followed by a string\n // provided by the target that is at least the same length.\n // escape special chars:\n const regexSafeSid = halfSid.replace(/[-\\/\\\\^$*+?.()|[\\\\]{}]/g, '\\\\$&');\n const sidRegex = new RegExp(`^${regexSafeSid}.{${halfSid.length},}$`);\n if (typeof resp.dcpsid !== 'string' || !resp.dcpsid.match(sidRegex)) {\n throw new DCPError(`Target responded with invalid DCPSID: ${resp.dcpsid}`, 'DCPC-1008');\n }\n debugging('connection') && console.debug(this.debugLabel, 'connection established.');\n return {\n dcpsid: resp.dcpsid,\n peerAddress: resp.owner,\n };\n }\n \n /**\n * Perform any remaining `establish` tasks\n * now that connection is fully ready.\n */\n finishSetup() {\n this.requestQueueService();\n }\n\n /**\n * We cannot use the normal enqueue logic for the first message\n * and we need many of the same bits of logic (but not all) so\n * this is the place for that kind of one-off logic.\n * @param {Message} message \n */\n async specialFirstSend(message) { /* XXXwg - special first send *will* cause double dcpsid if invoked twice */\n message.initSendProps();\n const messageSentPromise = this.messageLedger.addMessage(message);\n await this.createAndSend([message]); /* XXXwg - messageLedger may leak messages when createAndSend throws */\n if (message instanceof this.connection.Response) {\n this.connection.registerConnectResponse(this.inFlight.message); /* XXXwg todo - audit this.inFlight */\n }\n return messageSentPromise;\n }\n\n /**\n * Places message into `queue` but only schedules queue to be serviced\n * if connection is ready to send. Otherwise other tools will have to\n * handle making it ready to send again.\n * @param {Connection.Request|Connection.Response} message\n * @returns {Promise} from messageLedger\n * @resolves to a response if sending a request, or resolves when the response was sent.\n */\n enqueue(message) {\n /* Note - it looks like message.type is always undefined in here, but that somehow \n * the message is mutated between enqueue and transmission. /wg Nov 2021\n */\n if (message.payload)\n debugging('connection') && console.debug(this.debugLabel, `enqueueing ${message.type} message for operation:`, message.payload && message.payload.operation);\n else\n debugging('connection') && console.debug(this.debugLabel, `enqueueing ${message.type} message`);\n \n message.initSendProps();\n this.queue.push(message);\n this.requestQueueService();\n return this.messageLedger.addMessage(message);\n }\n\n /**\n * Kick off the send loops (outer and inner) on the next event cycle.\n * This is a 'request' because if the loop was already going to run\n * anyways this method has no effect.\n */\n requestQueueService () {\n if (this.requestQueueServiceTimeout) return;\n this.requestQueueServiceTimeout = setTimeout( () => {\n this.requestQueueServiceTimeout = null;\n this.serviceQueue();\n });\n }\n\n /**\n * Creates a batch and puts it in flight if the connection is established \n * and no batch is already in flight.\n */\n async serviceQueue () {\n if (!this.inFlight && this.connection.state.in(['established', 'closing', 'close-wait'])) {\n await this.createAndSend();\n } else {\n debugging('connection') && console.debug(this.debugLabel, `ignoring call to service queue. inFlight=${!!this.inFlight} state=${this.connection.state.valueOf()}`);\n }\n }\n\n /**\n * Creates a batch if it can find any messages to send and sends them.\n * If there are no messages to send, simply returns.\n * @param {Array<Connection.Message>} [messages] Array of messages to batch together for a transmission. \n * if not provided, draws from queue.\n */\n async createAndSend(messages=[]) {\n if (messages.length === 0) {\n messages = this.queue.splice(0, this.batchSize);\n debugging('connection') && console.debug(\n this.debugLabel, \n `pulled ${messages.length} message${messages.length === 1? '' : 's'} from queue.`\n );\n }\n if (messages.length === 0) return;\n assert(!this.inFlight);\n \n let op;\n let opIsReq = false;\n if (messages.length === 1) { // ie. no need to batch, can send single message\n op = messages[0];\n op.ackToken = this.makeAckToken();\n opIsReq = true;\n } else {\n op = new this.connection.Batch(messages, this.makeAckToken());\n }\n\n op.nonce = this.nonce;\n op.initSendProps();\n let signedOp = await op.sign();\n this.inFlight = { message: op, signedMessage: signedOp };\n if (!opIsReq)\n this.messageLedger.addBatch(op);\n // user can modify what is in flight, so we hop off the event loop /* XXXwg very dangerous - check spec */\n this.connection.emit('send', this.inFlight);\n await new Promise(r => setImmediate(r));\n // Connection may start shutting down while awaiting and sets this.inFlight=null\n if(!this.inFlight) return;\n // now we're back to sending\n debugging('connection') && console.debug(this.debugLabel, `sending a signed batch/request of ${messages.length}`);\n this.doSend();\n }\n \n /**\n * Sends the message stored in the `inFlight` var over the transport.\n * Schedules a future time to send it again.\n * `clearFlightDeck` is the only method that should be clearing this timer\n * and thus closing the loop.\n */\n doSend() {\n assert(this.inFlight);\n let op = this.inFlight.message;\n let signedOp = this.inFlight.signedMessage;\n try {\n op.updateSendProps();\n if (op instanceof this.connection.Request || op instanceof this.connection.Response) {\n debugging('connection') && console.debug(this.debugLabel, 'sending request/response. Next send in (ms):', op.msUntilNextSend);\n this.retryLoopTimeout = setTimeout(this.sendReqConditionally.bind(this), op.msUntilNextSend)\n } else if (op instanceof this.connection.Batch) {\n debugging('connection') && console.debug(this.debugLabel, 'sending batch. Next send in (ms):', op.msUntilNextSend);\n this.retryLoopTimeout = setTimeout(this.sendBatchConditionally.bind(this), op.msUntilNextSend);\n }\n this.connection.transport.send(signedOp);\n } catch (error) {\n console.error(`Error while signing and sending message to ${this.connection.url}:`, error);\n }\n }\n \n /**\n * Calls `doSend` if the request we are sending has not exceeded its limits\n */\n sendReqConditionally() {\n if (!this.inFlight)\n return\n let req = this.inFlight.message;\n if (req.overSendLimits) {\n debugging('connection') && console.debug(this.debugLabel, `Failing transport; reason: ${req.failReason}`);\n this.connection.failTransport(`Failing transport; reason: ${req.failReason}`); \n } else {\n this.doSend();\n }\n }\n\n /**\n * Calls `doSend` if the batch we are sending has not exceeded its limits\n */\n sendBatchConditionally() {\n // Prevent \"'message' of null\" error\n if (!this.inFlight)\n return;\n let batch = this.inFlight.message;\n if (batch.overSendLimits || (batch.messages[0].payload && batch.messages[0].payload.operation === 'connect')) {\n debugging('connection') && console.debug(this.debugLabel, `Failing transport; reason: ${batch.failReason}`);\n this.connection.failTransport(`Failing transport; reason: ${batch.failReason}`);\n } else {\n this.doSend();\n }\n }\n\n /**\n * Clear a message from the flight deck. For the foreseeable future the deck\n * only holds one message at a time, so we just assert that it matches.\n * @param {Connection.Message} message message that can be cleared from the flight deck\n */\n clearFlightDeck(message, nonce) {\n debugging('connection') && console.debug(this.debugLabel, 'clearing flight deck. nonce =', nonce);\n assert(message === this.inFlight.message);\n this.inFlight = null;\n this.nonce = nonce;\n clearTimeout(this.retryLoopTimeout);\n this.requestQueueService();\n }\n\n /**\n * When a connection is closed the sender needs to cancel its send efforts.\n */\n shutdown() {\n debugging('connection') && console.debug(this.debugLabel, 'shutting down.');\n this.inFlight = null;\n this.nonce = null;\n this.queue = [];\n clearTimeout(this.requestQueueServiceTimeout);\n clearTimeout(this.retryLoopTimeout);\n }\n\n // When allowBatch=false, set the batchSize to 1 so each\n // message gets sent individually (not in a batch)\n get batchSize() {\n if (this._batchSize) return this._batchSize;\n const batchSize = Math.max(this.connection.connectionOptions.maxMessagesPerBatch, 1);\n return this._batchSize = this.connection.connectionOptions.allowBatch ? batchSize : 1;\n }\n}\n\nObject.assign(module.exports, {\n Sender,\n});\n\n/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(/*! ./../../../node_modules/timers-browserify/main.js */ \"./node_modules/timers-browserify/main.js\").setImmediate))\n\n//# sourceURL=webpack:///./src/protocol-v4/connection/sender.js?");
|
|
5567
5567
|
|
|
5568
5568
|
/***/ }),
|
|
5569
5569
|
|