dcp-client 4.2.16 → 4.2.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.tidelift +2 -0
- package/dist/dcp-client-bundle.js +11 -11
- package/examples/nodejs/deploy-job.mjs +28 -0
- package/package.json +1 -2
|
@@ -3841,7 +3841,7 @@ eval("// Copyright Joyent, Inc. and other Node contributors.\n//\n// Permission
|
|
|
3841
3841
|
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
|
|
3842
3842
|
|
|
3843
3843
|
"use strict";
|
|
3844
|
-
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": () => (/* binding */ Modal)\n/* harmony export */ });\n/**\n * A Small Modal Class\n * @module Modal\n */\n/* globals Event dcpConfig */\nclass Modal {\n constructor (title, message, callback = false, exitHandler = false, {\n continueLabel = 'Continue',\n cancelLabel = 'Cancel',\n cancelVisible = true\n } = {}) {\n const modal = document.createElement('div')\n modal.className = 'dcp-modal-container-old day'\n modal.innerHTML = `\n <dialog class=\"dcp-modal-content\">\n <div class=\"dcp-modal-header\">\n <h2>${title}<button type=\"button\" class=\"close\">×</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\":\"1fa1b4aaf968907ebd2ddea4febd49e2e1e26d24\",\"branch\":\"release\",\"dcpClient\":{\"version\":\"4.2.16\",\"from\":\"git+ssh://git@gitlab.com/Distributed-Compute-Protocol/dcp-client.git#prod-20220919\",\"resolved\":\"git+ssh://git@gitlab.com/Distributed-Compute-Protocol/dcp-client.git#7f6f4c3347e9cd9d687f774d5149df5a54b76e31\"},\"built\":\"Thu Sep 29 2022 14:08:29 GMT-0400 (Eastern Daylight Saving Time)\",\"config\":{\"generated\":\"Thu 29 Sep 2022 02:08:29 PM EDT by erose on lorge\",\"build\":\"debug\"},\"webpack\":\"5.70.0\",\"node\":\"v14.20.0\"} !== 'undefined' && typeof window.Modal === 'undefined') {\n window.Modal = Modal\n }\n}\n\n\n//# sourceURL=webpack://dcp/./portal/www/js/modal.js?");
|
|
3844
|
+
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": () => (/* binding */ Modal)\n/* harmony export */ });\n/**\n * A Small Modal Class\n * @module Modal\n */\n/* globals Event dcpConfig */\nclass Modal {\n constructor (title, message, callback = false, exitHandler = false, {\n continueLabel = 'Continue',\n cancelLabel = 'Cancel',\n cancelVisible = true\n } = {}) {\n const modal = document.createElement('div')\n modal.className = 'dcp-modal-container-old day'\n modal.innerHTML = `\n <dialog class=\"dcp-modal-content\">\n <div class=\"dcp-modal-header\">\n <h2>${title}<button type=\"button\" class=\"close\">×</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\":\"bcdd0728ce28e9f58c7093651f0113bf0221c93a\",\"branch\":\"release\",\"dcpClient\":{\"version\":\"4.2.17\",\"from\":\"git+ssh://git@gitlab.com/Distributed-Compute-Protocol/dcp-client.git#prod-20221004\",\"resolved\":\"git+ssh://git@gitlab.com/Distributed-Compute-Protocol/dcp-client.git#9b75e977dc0b144e2e2e42ee9d12e063e46754f0\"},\"built\":\"Wed Oct 05 2022 10:04:37 GMT-0400 (Eastern Daylight Saving Time)\",\"config\":{\"generated\":\"Wed 05 Oct 2022 10:04:35 AM EDT by erose on lorge\",\"build\":\"debug\"},\"webpack\":\"5.70.0\",\"node\":\"v14.20.1\"} !== 'undefined' && typeof window.Modal === 'undefined') {\n window.Modal = Modal\n }\n}\n\n\n//# sourceURL=webpack://dcp/./portal/www/js/modal.js?");
|
|
3845
3845
|
|
|
3846
3846
|
/***/ }),
|
|
3847
3847
|
|
|
@@ -4147,7 +4147,7 @@ eval("/**\n * @file password.js\n * Modal providing a way to
|
|
|
4147
4147
|
\**********************************************/
|
|
4148
4148
|
/***/ ((__unused_webpack_module, exports, __webpack_require__) => {
|
|
4149
4149
|
|
|
4150
|
-
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=
|
|
4150
|
+
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=bcdd0728ce28e9f58c7093651f0113bf0221c93a'; /* installer token */\n return '?ucp=' + Date.now();\n}\n \n/* Detect load type - on webpack, load dynamic content relative to webpack bundle;\n * otherwise load relative to the current scheduler's configured portal.\n */\nexports.myScript = (typeof document !== 'undefined') && document.currentScript;\nexports.corsProxyHref = undefined;\nif (exports.myScript && exports.myScript === (__webpack_require__(/*! ./fetch-relative */ \"./src/dcp-client/client-modal/fetch-relative.js\").myScript)) {\n let url = new ((__webpack_require__(/*! dcp/common/dcp-url */ \"./src/common/dcp-url.js\").DcpURL))(exports.myScript.src);\n exports.corsProxyHref = url.resolve('../cors-proxy.html');\n}\n\n/**\n * Look for modal id and required ids on page based on config, if not found, provide from dcp-client.\n * The first id in the required array must be the id of the modal's form element.\n * @param {Object} modalConfig Modal configuration object\n * @param {string} modalConfig.id Id of parent modal element\n * @param {string[]} modalConfig.required Array of required ids in parent modal element\n * @param {string[]} [modalConfig.optional] Array of optional ids in parent modal element\n * @param {string} modalConfig.path Relative path to modal html in dcp-client\n * @returns {DOMElement[]} Array of modal elements on page [config.id, ...config.required]\n */\nexports.initModal = async function (modalConfig, onClose) {\n exports.corsProxyHref = exports.corsProxyHref || dcpConfig.portal.location.resolve('dcp-client/cors-proxy.html');\n\n // Call ensure modal on any eager-loaded modals.\n if (modalConfig.eagerLoad) {\n Promise.all(\n modalConfig.eagerLoad.map(config => ensureModal(config))\n )\n };\n\n const [elements, optionalElements] = await ensureModal(modalConfig);\n\n // Wire up form to prevent default, resolve on submission, reject+reset when closed (or call onClose when closed)\n const [modal, form] = elements;\n form.reset(); // ensure that form is fresh\n let formResolve, formReject;\n let formPromise = new Promise( function(res, rej) {\n formResolve = res;\n formReject = rej;\n });\n form.onsubmit = function (submitEvent) {\n submitEvent.preventDefault();\n modal.setAttribute(\"data-state\", \"submitted\");\n formResolve(submitEvent);\n }\n\n exports.MicroModal.show(modalConfig.id, { \n disableFocus: true, \n onClose: onClose || getDefaultOnClose(formReject)\n });\n return [elements, formPromise, optionalElements];\n};\n\n// Ensure all required modal elements are on page according to modalConfig\nasync function ensureModal(modalConfig) {\n let allRequiredIds = [modalConfig.id, ...modalConfig.required];\n let missing = allRequiredIds.filter( id => !document.getElementById(id) );\n if (missing.length > 0) {\n if (missing.length !== allRequiredIds.length)\n console.warn(`Some of the ids needed to replace the default DCP-modal were found, but not all. So the default DCP-Modal will be used. Missing ids are: [${missing}].`);\n let contents = await fetchRelative(exports.corsProxyHref, modalConfig.path + cachePoison());\n const container = document.createElement('div');\n container.innerHTML = contents;\n document.body.appendChild(container);\n }\n\n const elements = allRequiredIds.map(id => document.getElementById(id));\n const optionalElements = (modalConfig.optional || []).map(id => document.getElementById(id));\n return [elements, optionalElements];\n};\n\n// This onClose is called by MicroModal and thus has the modal passed to it.\nfunction getDefaultOnClose (formReject) {\n return (modal) => {\n modal.offsetLeft; // forces style recalc\n const origState = modal.dataset.state;\n // reset form including data-state\n modal.setAttribute(\"data-state\", \"new\");\n // reject if closed without submitting form.\n if (origState !== \"submitted\") {\n const err = new DCPError(\"Modal was closed but modal's form was not submitted.\", exports.OnCloseErrorCode);\n formReject(err);\n }\n }\n}\n\n\n//# sourceURL=webpack://dcp/./src/dcp-client/client-modal/utils.js?");
|
|
4151
4151
|
|
|
4152
4152
|
/***/ }),
|
|
4153
4153
|
|
|
@@ -4158,7 +4158,7 @@ eval("/**\n * @file client-modal/utils.js\n * @author KC Erb\n * @date Mar 2020\
|
|
|
4158
4158
|
/***/ ((__unused_webpack_module, exports, __webpack_require__) => {
|
|
4159
4159
|
|
|
4160
4160
|
"use strict";
|
|
4161
|
-
eval("/**\n * @file Client facing module that implements Compute Groups API\n * @module dcp/compute-groups\n * @access public\n * @author Kayra E-A <kayra@kingsds.network>\n * Wes Garland <wes@kingsds.network>\n * Paul <paul@kingsds.network>\n * @date Sept 2020\n * February 2022\n * May 2022\n */\n\n\nconst protocolV4 = __webpack_require__(/*! dcp/protocol-v4 */ \"./src/protocol-v4/index.js\");\nconst wallet = __webpack_require__(/*! dcp/dcp-client/wallet */ \"./src/dcp-client/wallet/index.js\");\nconst hash = __webpack_require__(/*! ../../common/hash */ \"./src/common/hash.js\");\nconst { DCPError } = __webpack_require__(/*! ../../common/dcp-error */ \"./src/common/dcp-error.js\");\nconst debugging = (__webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope)('scheduler');\nconst { Address } = __webpack_require__(/*! dcp/dcp-client/wallet */ \"./src/dcp-client/wallet/index.js\");\nconst constants = __webpack_require__(/*! dcp/common/scheduler-constants */ \"./src/common/scheduler-constants.js\");\nconst { clientError, reconstructServiceError } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\n\n/** @typedef {import('dcp/utils').apiServiceType} apiServiceType */\n/** @typedef {import('dcp/utils').apiClientType} apiClientType */\n/** @typedef {string} opaqueId */\n\n/**\n * @typedef {object} cgAccessType\n * @property {opaqueId} [id]\n * @property {string} [joinKey]\n */\n\n/**\n * @typedef {object} cgClientJoinType\n * @property {opaqueId} [id]\n * @property {Address} [joinAddress]\n * @property {string} [joinKey]\n * @property {string} [joinSecret]\n * @property {string} [joinHash]\n */\n\n/**\n * @typedef {object} cgServiceJoinType\n * @property {opaqueId} [id]\n * @property {Address} [joinAddress]\n * @property {string} [joinKey]\n * @property {string} [joinHashHash]\n */\n\n/**\n * Establishes the client connection to the computeGroups microservice if it does not exist already from the default config.\n * \n * @returns {protocolV4.Connection}\n * @access public\n * @example\n * const result = await exports.serviceConnection.send('createGroup', {\n name: name,\n description: description,\n });\n */\n\nexports.serviceConnection = null;\n\n//\n// Reference counting pattern:\n// For every time addRef is called,\n// closeServiceConnection must eventually be called.\n// Reference counting allows multiple execs in a Promise.all .\n//\nvar refCount = 0;\nexports.addRef = function addRef() {\n refCount++;\n}\n\nconst openAndConnectServiceConn = async function openAndConnectServiceConn()\n{\n exports.serviceConnection = new protocolV4.Connection(dcpConfig.scheduler.services.computeGroups);\n exports.serviceConnection.on('close', openAndConnectServiceConn);\n await exports.serviceConnection.connect();\n refCount = 0; // Help with sanity.\n}\n\n/**\n * Resets the client connection to the computeGroups microservice.\n */\nexports.closeServiceConnection = async function closeServiceConnection() {\n if (refCount > 0) refCount--;\n if (exports.serviceConnection && refCount < 1)\n {\n exports.serviceConnection.off('close', openAndConnectServiceConn);\n exports.serviceConnection.close(null, true);\n refCount = 0; // Help with sanity.\n exports.serviceConnection = null;\n }\n};\n\n/**\n * (Used in jobs/index.js)\n * KeepAlive for the service connection to compute groups.\n */\nexports.keepAlive = async function keepAlive() {\n if (!exports.serviceConnection)\n await openAndConnectServiceConn();\n\n exports.serviceConnection.keepalive().catch(err => console.error('Warning: keepalive failed for compute groups service', err));\n}\n\n/**\n * Checks whether descriptor corresponds to the public compute group from the scheduler constants.\n */\nexports.isPublicComputeGroup = function isPublicComputeGroup(descriptor) {\n return descriptor.id === constants.computeGroups.public.id\n && descriptor.opaqueId === constants.computeGroups.public.opaqueId;\n};\n\n/**\n * Returns a compute group identification snippet for diagnostic messages,\n * @param {object} descriptor - Must have one of the properties joinKey, id (id:=opaqueId). Specifically\n * descriptor = { joinKey: 'dcpDemo' } or descriptor = { id: 'bYcYGQ3NOpFnP4FKs6IBQd' },\n * where the corresponding row in table computeGroups have attributes\n * joinKey:='dcpDemo' or opaqueId:='bYcYGQ3NOpFnP4FKs6IBQd' .\n * @returns {string}\n */\nfunction cgId(descriptor) {\n return (descriptor.joinKey) ? `joinKey ${descriptor.joinKey}` : `id ${descriptor.id}`;\n}\n\n/**\n * Verify sufficient information in descriptor to access a compute group.\n * Emit diagnostics about unnecessary information.\n * @param {cgAccessType} descriptor \n * @param {string} methodName \n */\nfunction validateCGDescriptor(descriptor, methodName) {\n for (const prop in descriptor) {\n if ([ 'id', 'joinKey' ].includes(prop)) continue;\n if ([ 'joinAddress', 'joinHash', 'joinSecret' ].includes(prop))\n console.warn(`It is not necessary to specify '${prop}' in the descriptor ${JSON.stringify(descriptor)} when calling ${methodName}`);\n else\n console.error(`Do not specify '${prop}' in the descriptor ${JSON.stringify(descriptor)} when calling ${methodName}`);\n }\n}\n\n/**\n * Verify sufficient information in descriptor to authorize a compute group.\n * Emit diagnostics about unnecessary information.\n * @param {cgClientJoinType} joinDescriptor \n * @param {string} methodName \n */\nfunction validateCGJoinDescriptor(joinDescriptor, methodName) {\n for (const prop in joinDescriptor) {\n if ([ 'id', 'joinKey', 'joinSecret', 'joinHash', 'joinAddress' ].includes(prop)) continue;\n console.error(`Do not specify '${prop}' in the descriptor ${JSON.stringify(joinDescriptor)} when calling ${methodName}`);\n }\n}\n\n/**\n * Build message to go across the wire.\n * Verify sufficient information in descriptor to access a compute group.\n * Emit diagnostics about unnecessary information.\n * @param {cgAccessType} descriptor\n * @param {string} methodName\n * @returns {cgAccessType}\n */\nfunction buildCGMessage(descriptor, methodName)\n{\n if (exports.isPublicComputeGroup(descriptor)) return descriptor;\n\n const message = {};\n // Construct message.joinKey xor message.id .\n if (descriptor.joinKey) message.joinKey = descriptor.joinKey;\n else if (descriptor.id) message.id = descriptor.id; // id:=opaqueId\n\n debugging('computeGroups') && console.debug(`${methodName}:buildCGMessage: descriptor`, descriptor, 'message', message);\n\n validateCGDescriptor(descriptor, methodName);\n\n return message;\n}\n\n/**\n * Build message so that joinHash, joinSecret, opaqueId do not go across the wire.\n * Verify sufficient information in descriptor to authorize a compute group.\n * Emit diagnostics about unnecessary information.\n * @param {cgClientJoinType} descriptor\n * @param {string} methodName\n * @returns {cgServiceJoinType}\n */\nfunction buildCGJoinMessage(descriptor, methodName)\n{\n if (exports.isPublicComputeGroup(descriptor)) return descriptor;\n\n const message = {};\n // Construct message.joinKey xor message.id .\n if (descriptor.joinKey) message.joinKey = descriptor.joinKey;\n else if (descriptor.id) message.id = descriptor.id; // id:=opaqueId\n // Construct message.joinAddress .\n if (descriptor.joinAddress) message.joinAddress = descriptor.joinAddress;\n\n debugging('computeGroups') && console.debug(`${methodName}:buildCGJoinMessage: descriptor`, descriptor, 'message', message);\n\n validateCGJoinDescriptor(descriptor, methodName);\n\n // Construct message.joinHashHash .\n if (descriptor.joinSecret) message.joinHashHash = hash.calculate(hash.eh1, exports.calculateJoinHash(descriptor), exports.serviceConnection.dcpsid);\n if (descriptor.joinHash) message.joinHashHash = hash.calculate(hash.eh1, descriptor.joinHash, exports.serviceConnection.dcpsid);\n\n return message;\n}\n\nfunction hasSufficientJoinInfo(joinDescriptor) {\n // Verify joinDescriptor has sufficient information to authorize a compute group (not guarenteed).\n return (joinDescriptor.joinKey && (joinDescriptor.joinSecret || joinDescriptor.joinHash))\n || (joinDescriptor.id && joinDescriptor.joinAddress)\n || exports.isPublicComputeGroup(joinDescriptor);\n}\n\nconst newCGPrototype = { type: 'object',\n parameters: {\n // name: { type: 'string', default: undefined }, /* name of group (length <= 255) */\n // description: { type: 'string', default: undefined }, /* description of group (length <= 255) */\n // id: { type: 'string', default: undefined }, /* opaqueId, the unique identifier of the compute group; nanoid (length === 22) */\n // joinKey: { type: 'string', default: undefined }, /* basically the login (length <= 255) */\n // joinSecret: { type: 'string', default: undefined }, /* basically the password (length <= 255) */\n // joinHash: { type: 'string', default: undefined }, /* basically the password, the joinSecret seeded & hashed */\n // joinAddress: { type: Address, default: undefined }, /* signature gives alternative to login/password */\n\n commissionRate: { type: 'BigNumber', default: undefined }, /* commission, see DCP-1889 */\n deployFee: { type: 'BigNumber', default: undefined }, /* number of DCC to take for every deployment */\n deployAccess: { type: 'string', default: undefined }, /* can be \"owner\"|\"join\" (dcp-1910) */\n addJobFee: { type: 'BigNumber', default: undefined }, /* fee required each time a job joins a compute group */\n maxTotalPayment: { type: 'BigNumber', default: undefined }, /* limit on maximum job payment, NULL => Infinity */\n\n /* Administrative limits on group. NULL => Infinity: Should all be integers or undefined. */\n maxConcurrentJobs: { type: 'number', default: undefined },\n maxConcurrentWorkers: { type: 'number', default: undefined },\n maxConcurrentSandboxes: { type: 'number', default: undefined },\n maxConcurrentCPUs: { type: 'number', default: undefined },\n maxConcurrentGPUs: { type: 'number', default: undefined },\n maxConcurrentEscrow: { type: 'BigNumber', default: undefined },\n },\n};\n\n/**\n * Async function that creates a new Compute Group.\n *\n * The joinDescriptor is of the form { joinKey, joinSecret }, { joinKey, joinHash } or { id, joinAddress }.\n * where id will correspond to the attribute opaqueId in the new row in the computeGroups table.\n *\n * This function can only be called with ADMIN permission.\n * Properties not appearing in newCGPrototype.parameters are not allowed in otherProperties.\n *\n * @param {cgClientJoinType} joinDescriptor - Must have properly defined { joinKey, joinSecret }, { joinKey, joinHash }\n * or { id, joinAddress }, where id will correspond to the attribute opaqueId\n * in the new row in the computeGroups table.\n * @param {string} [name] - The name of the compute group.\n * @param {string} [description] - The description of the compute group.\n * @param {object} [otherProperties] - The 5 attributes of table computeGroup related to commissions and fees.\n * commissionRate: notNull(zFinNum),// commission, see DCP-1889\n * deployFee: notNull(zFinNum),// number of DCC to take for every deployment\n * deployAccess: string, // can be \"owner\"|\"join\" (dcp-1910)\n * addJobFee: notNull(zFinNum),// fee required each time a job joins a compute group\n * maxTotalPayment: finNum, // limit on maximum job payment, NULL => Infinity\n * And the 6 attributes of table computeGroup related to limits.\n * maxConcurrentJobs: integer,\n * maxConcurrentWorkers: integer,\n * maxConcurrentSandboxes: integer,\n * maxConcurrentCPUs: integer,\n * maxConcurrentGPUs: integer,\n * maxConcurrentEscrow: finNum,\n * @returns {Promise<apiClientType>} - { success, payload: computeGroup.id }\n * @access public\n * @example\n * await computeGroup.createGroup({ joinKey: 'dcpDemo', joinSecret: 'theSecret' }, 'myCGName', 'myCGDescription', { deployFee: 0.00015 });\n * await computeGroup.createGroup({ joinKey: 'dcpDemo2', joinHash: 'eh1-e063976b20a92da97a27b9873465c6f2c9d6e4370befa86c8c1dd312c78befc2' });\n * await computeGroup.createGroup({ id: 'bYcYGQ3NOpFnP4FKs6IBQd', joinAddress: 'c15053fc30d4bdf91e2e0bba79578f8b649e55ea' });\n * where the corresponding rows in table computeGroups have attributes\n * row1: joinKey:='dcpDemo',\n * row2: joinKey:='dcpDemo2', joinHash:='eh1-e063976b20a92da97a27b9873465c6f2c9d6e4370befa86c8c1dd312c78befc2'\n * row3: opaqueId:='bYcYGQ3NOpFnP4FKs6IBQd', joinAddress:='c15053fc30d4bdf91e2e0bba79578f8b649e55ea' .\n */\nexports.createGroup = async function createGroup(joinDescriptor, name, description, otherProperties)\n{\n if (!exports.serviceConnection)\n await openAndConnectServiceConn();\n\n validateCGJoinDescriptor(joinDescriptor, 'createGroup');\n\n // Verify joinDescriptor has sufficient information to authorize a compute group (not guarenteed).\n if (!hasSufficientJoinInfo(joinDescriptor))\n return clientError(`createGroup: Insufficient information to authorize compute group: ${JSON.stringify(joinDescriptor)}.`);\n\n // Validate the properties in otherProperties.\n for (const methodName in otherProperties) {\n if (!Object.keys(newCGPrototype.parameters).includes(methodName))\n return clientError(`createGroup: Property ${methodName} cannot be speicfied in otherProperties. Can only specify ${JSON.stringify(Object.keys(newCGPrototype.parameters))}`);\n }\n\n // Translate joinSecret to joinHash.\n if (joinDescriptor.joinSecret) {\n joinDescriptor.joinHash = exports.calculateJoinHash(joinDescriptor);\n delete joinDescriptor.joinSecret;\n }\n\n if (otherProperties && (otherProperties.commissionRate < 0 || otherProperties.commissionRate >= 1))\n return clientError(`client-createGroup: commissionRate ${otherProperties.commissionRate} must be between 0 and 1 (0 <= commissionRate < 1).`);\n\n debugging('computeGroups') && console.debug('client-createGroup: input:', joinDescriptor, name, description, otherProperties);\n\n const { success, payload } = await exports.serviceConnection.send('createGroup', { joinDescriptor, name, description, otherProperties });\n\n if (!success) return clientError(`Cannot create new compute group, with ${cgId(joinDescriptor)}.`);\n if (payload && !payload.success) return reconstructServiceError(payload);\n\n debugging('computeGroups') && console.debug('client-createGroup: payload', payload);\n\n return payload;\n};\n\nconst changeCGPrototype = { type: 'object',\n parameters: {\n name: { type: 'string', default: undefined }, /* name of group (length <= 255) */\n description: { type: 'string', default: undefined }, /* description of group (length <= 255) */\n joinHash: { type: 'string', default: undefined }, /* basically the password, seeded & hashed (length <= 255) */\n joinAddress: { type: Address, default: undefined }, /* signature gives alternative to login/password */\n\n commissionRate: { type: 'BigNumber', default: undefined }, /* commission, see DCP-1889 */\n deployFee: { type: 'BigNumber', default: undefined }, /* number of DCC to take for every deployment */\n deployAccess: { type: 'string', default: undefined }, /* can be \"owner\"|\"join\" (dcp-1910) */\n addJobFee: { type: 'BigNumber', default: undefined }, /* fee required each time a job joins a compute group */\n maxTotalPayment: { type: 'BigNumber', default: undefined }, /* limit on maximum job payment, NULL => Infinity */\n\n /* Administrative limits on group. NULL => Infinity: Should all be integers or undefined. */\n maxConcurrentJobs: { type: 'number', default: undefined },\n maxConcurrentWorkers: { type: 'number', default: undefined },\n maxConcurrentSandboxes: { type: 'number', default: undefined },\n maxConcurrentCPUs: { type: 'number', default: undefined },\n maxConcurrentGPUs: { type: 'number', default: undefined },\n maxConcurrentEscrow: { type: 'BigNumber', default: undefined },\n },\n};\n\n/**\n * Async function that changes a new Compute Group.\n * \n * The parameter newDescriptor contains the new property values,\n * and the properties that are allowed to be changed appear in changeCGPrototype.parameters.\n * \n * The descriptor must have joinKey or id, where id:=opaqueId.\n * Must own the compute group or be ADMIN to use changeGroup.\n * \n * @param {cgAccessType} descriptor - Must have joinkey or id, where id:=opaqueId.\n * @param {object} newDescriptor - Properties not appearing in changeCGPrototype.parameters are not allowed.\n * @returns {Promise<apiClientType>}\n * await computeGroup.changeGroup({ joinKey: 'dcpDemo' }, { joinSecret: 'myNewPasswrd' });\n * await computeGroup.changeGroup({ id: 'bYcYGQ3NOpFnP4FKs6IBQd' }, { name: 'myNewName', deployFee: 0.0001 });\n * where the corresponding rows in table computeGroups have attributes\n * row1: joinKey:='dcpDemo',\n * row2: opaqueId:='bYcYGQ3NOpFnP4FKs6IBQd'\n */\nexports.changeGroup = async function changeGroup(descriptor, newDescriptor)\n{\n if (!exports.serviceConnection)\n await openAndConnectServiceConn();\n\n // Verify descriptor has sufficient information to access a compute group (not guarenteed).\n if (!descriptor.joinKey && !descriptor.id)\n return clientError(`changeGroup: Insufficient information to identify compute group: ${JSON.stringify(descriptor)}.`);\n\n // Validate the properties in newDescriptor.\n for (const methodName in newDescriptor) {\n if (!Object.keys(changeCGPrototype.parameters).includes(methodName))\n return clientError(`changeGroup: Not allowed to change property ${methodName}. Can only change ${JSON.stringify(Object.keys(changeCGPrototype.parameters))}`);\n }\n\n // Translate joinSecret to joinHash.\n if (newDescriptor.joinSecret) {\n newDescriptor.joinHash = exports.calculateJoinHash(newDescriptor);\n delete newDescriptor.joinSecret;\n }\n\n descriptor = buildCGMessage(descriptor, 'changeGroup');\n debugging('computeGroups') && console.debug('change compute group client:', descriptor, newDescriptor);\n const { success, payload } = await exports.serviceConnection.send('changeGroup', { descriptor, newDescriptor });\n\n if (!success) throw new DCPError(`Cannot change compute group with ${cgId(descriptor)}:`, payload);\n if (payload && !payload.success) return reconstructServiceError(payload);\n\n return payload;\n};\n\n/**\n * Async function that deletes a compute group.\n * \n * The descriptor must have joinkey or id, where id:=opaqueId.\n * \n * Must either own the group or be ADMIN.\n * If not ADMIN, then the following config must be true:\n * dcpConfig.scheduler.services.computeGroups.usersCanDeleteGroups\n * \n * @param {cgAccessType} descriptor - Must contain joinKey or id (id:=opaqueId) \n * @returns {Promise<apiClientType>}\n * await computeGroup.deleteGroup({ joinKey: 'dcpDemo' });\n * await computeGroup.deleteGroup({ id: 'bYcYGQ3NOpFnP4FKs6IBQd' });\n * where the corresponding rows in table computeGroups have attributes\n * row1: joinKey:='dcpDemo',\n * row2: opaqueId:='bYcYGQ3NOpFnP4FKs6IBQd'\n */\nexports.deleteGroup = async function deleteGroup(descriptor)\n{\n if (!exports.serviceConnection)\n await openAndConnectServiceConn();\n\n // Verify descriptor has sufficient information to access a compute group (not guarenteed).\n if (!descriptor.joinKey && !descriptor.id)\n return clientError(`deleteGroup: Insufficient information to identify compute group: ${JSON.stringify(descriptor)}.`);\n\n descriptor = buildCGMessage(descriptor, 'deleteGroup');\n debugging('computeGroups') && console.debug('delete compute group client:', descriptor);\n const { success, payload } = await exports.serviceConnection.send('deleteGroup', { descriptor });\n\n if (!success) throw new DCPError(`Cannot delete compute group with ${cgId(descriptor)}:`, payload);\n if (payload && !payload.success) return reconstructServiceError(payload);\n\n return payload;\n};\n\n/**\n * Async function that adds a job to a specified compute group. \n * \n * Must be the owner of the job.\n *\n * Useful feedback is provided from this function, as it\n * will make its way back to the application developer, *after* they have made the\n * deployment fee micropayment.\n *\n * On the client side the access model in place is that if you know the (user/password)\n * joinKey+joinSecret/joinKey+joinHash/joinKey+joinHashHash/id+joinAddress,\n * you can add the job to the compute groups, where id:=opaqueId from table computeGroups.\n * On the service side the corresponding access model is\n * joinKey+joinHashHash/id+joinAddress .\n * Access is also allowed if the compute group owner is the connection peerAddress.\n * \n * Unless the compute group owner is the connection peerAddress, element of the descriptor array must contain\n * { joinKey, joinSecret }, { joinKey, joinHash } or { id, joinAddress }\n * where the value of id in { id, joinAddress } is the opaqueId attribute of the row in table computeGroups.\n *\n * @param {Address} job The address of the Job that will be added to the Compute Group.\n * @param {Array} computeGroups Array of descriptor objects for the compute groups. This descriptor\n * needs to contain enough information to authorize access to the\n * compute group. Properties may include:\n * - id (id:=opaqueId)\n * - joinKey\n * - joinSecret\n * - joinHash\n * - joinAddress\n * \n * Additional, either the joinKey or id MUST be specified so\n * that we can identify the compute group in question.\n *\n * All compute groups can have jobs submitted to them, provided either the joinKey\n * or the id are specified, and the message contains valid join permission and the \n * job is owned by the caller of addJobToGroups.\n *\n * FUTURE - after DCP-1910\n * keystore A keystore used to grant access to job deployment within this compute group.\n * This can be either the ownerKeystore or the joinAddress keystore when the\n * compute group is in deployAccessType='join' mode.\n * @returns {Promise<apiClientType>}\n * @access public\n * @example\n * await computeGroup.addJobToGroups('P+Y4IApeFQLrYS2W7MkVg7', \n * [ { joinKey: 'dcpDemo', joinSecret: 'theSecret' },\n * { joinKey: 'dcpDemo2', joinHash: 'eh1-e063976b20a92da97a27b9873465c6f2c9d6e4370befa86c8c1dd312c78befc2' }, \n * { id: 'bYcYGQ3NOpFnP4FKs6IBQd', joinAddress: 'c15053fc30d4bdf91e2e0bba79578f8b649e55ea' } ]);\n * where the corresponding rows in table computeGroups have attributes\n * row1: joinKey:='dcpDemo',\n * row2: joinKey:='dcpDemo2', joinHash:='eh1-e063976b20a92da97a27b9873465c6f2c9d6e4370befa86c8c1dd312c78befc2'\n * row3: opaqueId:='bYcYGQ3NOpFnP4FKs6IBQd', joinAddress:='c15053fc30d4bdf91e2e0bba79578f8b649e55ea' .\n */\nexports.addJobToGroups = async function addJobToGroups(job, computeGroups)\n{\n if (!exports.serviceConnection)\n await openAndConnectServiceConn();\n\n const cgArray = [];\n for (const joinDescriptor of computeGroups)\n {\n // Verify joinDescriptor has sufficient information to authorize a compute group (not guarenteed).\n if (!hasSufficientJoinInfo(joinDescriptor))\n {\n console.error(`addJobToGroups: Insufficient information to authorize compute group: ${JSON.stringify(joinDescriptor)}.`);\n continue;\n }\n\n // Translate so that neither joinHash nor joinSecret goes across the wire.\n const message = buildCGJoinMessage(joinDescriptor, 'addJobToGroups');\n debugging('computeGroups') && console.debug(`addJobToGroups client: job ${job}, message`, message);\n\n cgArray.push(message);\n }\n\n const { success, payload } = await exports.serviceConnection.send('addJobToGroups', { job, cgArray });\n\n debugging('computeGroups') && console.debug('addJobToGroups payload', payload);\n\n if (!success) throw new DCPError(`Cannot add job ${job} to compute groups.`, payload);\n if (payload && !payload.success) return reconstructServiceError(payload);\n\n // If the server reported success but did not return a list of CGs (eg. v.4.2.5 server),\n // assume (and inform the client) we added all groups successfully\n return payload || computeGroups;\n};\n\n/**\n * Async function that lists all the Jobs in a Compute Group.\n * \n * The descriptor must have one of the properties joinkey, id (id:=opaqueId).\n * Must be the owner of the Compute Group to list jobs from it.\n * The job does not need to be owned.\n * \n * The descriptor is of the form { joinKey: 'dcpDemo' } or { id: 'bYcYGQ3NOpFnP4FKs6IBQd' }.\n * where 'bYcYGQ3NOpFnP4FKs6IBQd' is the opaqueId of the Compute Group.\n *\n * @param {cgAccessType} descriptor - Must have one of the properties joinKey, id (id:=opaqueId). Specifically\n * descriptor = { joinKey: 'dcpDemo' } or descriptor = { id: opaqueId }\n * @returns {Promise<apiClientType>}\n * @access public\n * @example\n * let listOfJobs1 = await computeGroup.listJobs({ joinKey: 'dcpDemo' });\n * let listOfJobs2 = await computeGroup.listJobs({ id: 'bYcYGQ3NOpFnP4FKs6IBQd' });\n * where the corresponding rows in table computeGroups have attributes\n * row1: joinKey:='dcpDemo'\n * row2: opaqueId:='bYcYGQ3NOpFnP4FKs6IBQd'\n */\nexports.listJobs = async function listJobs(descriptor)\n{\n if (!exports.serviceConnection)\n await openAndConnectServiceConn();\n\n // Verify descriptor has sufficient information to access a compute group (not guarenteed).\n if (!descriptor.joinKey && !descriptor.id)\n return clientError(`listJobs: Insufficient information to identify compute group: ${JSON.stringify(descriptor)}.`);\n\n descriptor = buildCGMessage(descriptor, 'listJobs');\n debugging('computeGroups') && console.debug('listJob client: descriptor', descriptor);\n const { success, payload } = await exports.serviceConnection.send('listJobs', { descriptor });\n\n if (!success) throw new DCPError(`Cannot list jobs for compute group with ${cgId(descriptor)}`, payload);\n if (payload && !payload.success) return reconstructServiceError(payload);\n\n return payload;\n};\n\n/**\n * Async function that removes a job from a Compute Group.\n * \n * The descriptor must have one of the properties joinkey, id (id:=opaqueId).\n * Must be the owner of the Compute Group to remove a job from it.\n * The job does not need to be owned.\n * \n * The descriptor is of the form { joinKey: 'dcpDemo' } or { id: 'bYcYGQ3NOpFnP4FKs6IBQd' }.\n * where 'bYcYGQ3NOpFnP4FKs6IBQd' is the opaqueId of the Compute Group.\n *\n * @param {Address} job - The address of the Job that will be added to the Compute Group.\n * @param {cgAccessType} descriptor - { joinKey: 'dcpDemo' } or { id: 'bYcYGQ3NOpFnP4FKs6IBQd' }\n * @returns {Promise<apiClientType>}\n * @access public\n * @example\n * await computeGroup.removeJob( 'P+Y4IApeFQLrYS2W7MkVg7', { joinKey: 'dcpDemo' });\n * await computeGroup.removeJob( 'P+Y4IApeFQLrYS2W7MkVg7', { id: 'bYcYGQ3NOpFnP4FKs6IBQd' });\n * where the corresponding rows in table computeGroups have attributes\n * row1: joinKey:='dcpDemo'\n * row2: opaqueId:='bYcYGQ3NOpFnP4FKs6IBQd'\n */\nexports.removeJob = async function removeJob(job, descriptor)\n{\n if (!exports.serviceConnection)\n await openAndConnectServiceConn();\n\n // Verify descriptor has sufficient information to access a compute group (not guarenteed).\n if (!descriptor.joinKey && !descriptor.id)\n return clientError(`removeJob: Insufficient information to identify compute group: ${JSON.stringify(descriptor)}.`);\n\n descriptor = buildCGMessage(descriptor, 'removeJob');\n debugging('computeGroups') && console.debug(`removeJob client: job ${job}, descriptor`, descriptor);\n const { success, payload } = await exports.serviceConnection.send('removeJob', { job, descriptor });\n\n if (!success) throw new DCPError(`Cannot remove job ${job} from compute group with ${cgId(descriptor)}`, payload);\n if (payload && !payload.success) return reconstructServiceError(payload);\n\n return payload;\n};\n\n/**\n * Async function that removes all jobs from a Compute Group.\n * \n * The descriptor must have one of the properties joinkey, id (id:=opaqueId).\n * Must be the owner of the Compute Group to remove jobs from it.\n * \n * The descriptor is of the form { joinKey: 'dcpDemo' } or { id: 'bYcYGQ3NOpFnP4FKs6IBQd' }.\n * where 'bYcYGQ3NOpFnP4FKs6IBQd' is the opaqueId of the Compute Group.\n *\n * @param {cgAccessType} descriptor - { joinKey: 'dcpDemo' } or { id: 'bYcYGQ3NOpFnP4FKs6IBQd' }\n * @returns {Promise<apiClientType>}\n * @access public\n * @example\n * await computeGroup.removeAllJobs({ joinKey: 'dcpDemo' });\n * await computeGroup.removeAllJobs({ id: 'bYcYGQ3NOpFnP4FKs6IBQd' });\n * where the corresponding rows in table computeGroups have attributes\n * row1: joinKey:='dcpDemo'\n * row2: opaqueId:='bYcYGQ3NOpFnP4FKs6IBQd'\n */\nexports.removeAllJobs = async function removeAllJobs(descriptor)\n{\n if (!exports.serviceConnection)\n await openAndConnectServiceConn();\n\n // Verify descriptor has sufficient information to access a compute group (not guarenteed).\n if (!descriptor.joinKey && !descriptor.id)\n return clientError(`removeAllJobs: Insufficient information to identify compute group: ${JSON.stringify(descriptor)}.`);\n\n descriptor = buildCGMessage(descriptor, 'removeAllJobs');\n debugging('computeGroups') && console.debug('removeAllJobs client: descriptor', descriptor);\n const { success, payload } = await exports.serviceConnection.send('removeAllJobs', { descriptor });\n\n if (!success) throw new DCPError(`Cannot remove all jobs from compute group with ${cgId(descriptor)}:`, payload);\n if (payload && !payload.success) return reconstructServiceError(payload);\n\n return payload;\n};\n\n/**\n * Async function that cancels the specified owned job.\n * \n * Must be the owner of the job.\n *\n * On the client side the access model in place is that if you know the (user/password)\n * joinKey+joinSecret/joinKey+joinHash/joinKey+joinHashHash/id+joinAddress,\n * you can cancel the job in the compute group, where id:=opaqueId from table computeGroups.\n * On the service side the corresponding access model is\n * joinKey+joinHashHash/id+joinAddress .\n * Access is also allowed if the compute group owner is the connection peerAddress.\n * \n * Unless the compute group owner is the connection peerAddress, the descriptor must contain\n * { joinKey, joinHashHash } or { id, joinAddress }\n * where the value of id in { id, joinAddress } is the opaqueId attribute of the row in table computeGroups.\n * \n * @param {Address} job - The address of the Job that will be added to the Compute Group.\n * @param {cgClientJoinType} joinDescriptor - Array of descriptor objects for the compute groups. This descriptor\n * needs to contain enough information to authorize access to the\n * compute group. Properties may include:\n * - id (id:=opaqueId)\n * - joinKey\n * - joinSecret\n * - joinHash\n * - joinAddress\n *\n * Additional, either the joinKey or id MUST be specified so\n * that we can identify the compute group in question.\n * @returns {Promise<apiClientType>}\n * @access public\n * @example\n * await computeGroup.cancelJob( 'P+Y4IApeFQLrYS2W7MkVg7', { joinKey: 'dcpDemo', joinHash: 'eh1-e063976b20a92da97a27b9873465c6f2c9d6e4370befa86c8c1dd312c78befc2' } );\n * await computeGroup.cancelJob( 'P+Y4IApeFQLrYS2W7MkVg7', { id: 'bYcYGQ3NOpFnP4FKs6IBQd', joinAddress: 'c15053fc30d4bdf91e2e0bba79578f8b649e55ea' });\n * where the corresponding rows in table computeGroups have attributes\n * row1: joinKey:='dcpDemo', joinHash:='eh1-e063976b20a92da97a27b9873465c6f2c9d6e4370befa86c8c1dd312c78befc2'\n * row2: opaqueId:='bYcYGQ3NOpFnP4FKs6IBQd', joinAddress:='c15053fc30d4bdf91e2e0bba79578f8b649e55ea' .\n */\nexports.cancelJob = async function cancelJob(job, joinDescriptor)\n{\n if (!exports.serviceConnection)\n await openAndConnectServiceConn();\n\n // Verify joinDescriptor has sufficient information to authorize a compute group (not guarenteed).\n if (!hasSufficientJoinInfo(joinDescriptor))\n return clientError(`cancelJob: Insufficient information to authorize compute group: ${JSON.stringify(joinDescriptor)}.`);\n\n // Translate so that neither joinHash nor joinSecret goes across the wire.\n joinDescriptor = buildCGJoinMessage(joinDescriptor, 'cancelJob');\n debugging('computeGroups') && console.debug(`cancelJob client: job ${job}, descriptor`, joinDescriptor);\n const { success, payload } = await exports.serviceConnection.send('cancelJob', { job, joinDescriptor });\n\n if (!success) throw new DCPError(`Cannot cancel job ${job} for compute group with ${cgId(joinDescriptor)}:`, payload);\n if (payload && !payload.success) return reconstructServiceError(payload);\n\n return payload;\n};\n\n/**\n * Async function that cancels the owned job in the Compute Group.\n * \n * On the client side the access model in place is that if you know the (user/password)\n * joinKey+joinSecret/joinKey+joinHash/joinKey+joinHashHash/id+joinAddress,\n * you can cancel the jobs in the compute group, where id:=opaqueId from table computeGroups.\n * On the service side the corresponding access model is\n * joinKey+joinHashHash/id+joinAddress .\n * Access is also allowed if the compute group owner is the connection peerAddress.\n * \n * Unless the compute group owner is the connection peerAddress, the descriptor must contain\n * { joinKey, joinHashHash } or { id, joinAddress }\n * where the value of id in { id, joinAddress } is the opaqueId attribute of the row in table computeGroups.\n * \n * @param {cgClientJoinType} joinDescriptor - Array of descriptor objects for the compute groups. This descriptor\n * needs to contain enough information to authorize access to the\n * compute group. Properties may include:\n * - id (id:=opaqueId)\n * - joinKey\n * - joinSecret\n * - joinHash\n * - joinAddress\n * \n * Additional, either the joinKey or id MUST be specified so\n * that we can identify the compute group in question.\n * @returns {Promise<apiClientType>}\n * @access public\n * @example\n * await computeGroup.cancelAllJobs( { joinKey: 'dcpDemo', joinHash: 'eh1-e063976b20a92da97a27b9873465c6f2c9d6e4370befa86c8c1dd312c78befc2' } );\n * await computeGroup.cancelAllJobs( { id: 'bYcYGQ3NOpFnP4FKs6IBQd', joinAddress: 'c15053fc30d4bdf91e2e0bba79578f8b649e55ea' });\n * where the corresponding rows in table computeGroups have attributes\n * row1: joinKey:='dcpDemo', joinHash:='eh1-e063976b20a92da97a27b9873465c6f2c9d6e4370befa86c8c1dd312c78befc2'\n * row2: opaqueId:='bYcYGQ3NOpFnP4FKs6IBQd', joinAddress:='c15053fc30d4bdf91e2e0bba79578f8b649e55ea' .\n */\nexports.cancelAllJobs = async function cancelAllJobs(joinDescriptor)\n{\n if (!exports.serviceConnection)\n await openAndConnectServiceConn();\n\n // Verify joinDescriptor has sufficient information to authorize a compute group (not guarenteed).\n if (!hasSufficientJoinInfo(joinDescriptor))\n return clientError(`cancelAllJobs: Insufficient information to authorize compute group: ${JSON.stringify(joinDescriptor)}.`);\n\n // Translate so that neither joinHash nor joinSecret goes across the wire.\n joinDescriptor = buildCGJoinMessage(joinDescriptor, 'cancelAllJobs');\n debugging('computeGroups') && console.debug('cancelAllJobs client: descriptor', joinDescriptor);\n const { success, payload } = await exports.serviceConnection.send('cancelAllJobs', { joinDescriptor });\n\n if (!success) throw new DCPError(`Cannot cancel owned jobs for compute group with ${cgId(joinDescriptor)}:`, payload);\n if (payload && !payload.success) return reconstructServiceError(payload);\n\n return payload;\n};\n\n/**\n * Calculate a joinHash for a compute group. This is an eh1- hash of the cg salt and \n * joinSecret components of a compute group description.\n *\n * @param {object} details an object containing the cg salt, which is\n * the joinKey if the compute group uses one;\n * otherwise it is the joinAddress. This object\n * may also contain the joinSecret.\n * @param {string} [joinSecret] the join secret -- plain text -- that is\n * the \"password\" for the compute group. If not\n * specified, we use details.joinSecret.\n */\nexports.calculateJoinHash = function computeGroups$calculateJoinHash(details, joinSecret)\n{\n if (typeof joinSecret === 'undefined')\n joinSecret = details.joinSecret;\n\n return hash.calculate(hash.eh1, `${details.joinKey || details.joinAddress} ${joinSecret}`);\n}\n\n\n//# sourceURL=webpack://dcp/./src/dcp-client/compute-groups/index.js?");
|
|
4161
|
+
eval("/**\n * @file Client facing module that implements Compute Groups API\n * @module dcp/compute-groups\n * @access public\n * @author Kayra E-A <kayra@kingsds.network>\n * Wes Garland <wes@kingsds.network>\n * Paul <paul@kingsds.network>\n * @date Sept 2020\n * February 2022\n * May 2022\n */\n\n\nconst protocolV4 = __webpack_require__(/*! dcp/protocol-v4 */ \"./src/protocol-v4/index.js\");\nconst wallet = __webpack_require__(/*! dcp/dcp-client/wallet */ \"./src/dcp-client/wallet/index.js\");\nconst hash = __webpack_require__(/*! ../../common/hash */ \"./src/common/hash.js\");\nconst { DCPError } = __webpack_require__(/*! ../../common/dcp-error */ \"./src/common/dcp-error.js\");\nconst debugging = (__webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope)('scheduler');\nconst { Address } = __webpack_require__(/*! dcp/dcp-client/wallet */ \"./src/dcp-client/wallet/index.js\");\nconst constants = __webpack_require__(/*! dcp/common/scheduler-constants */ \"./src/common/scheduler-constants.js\");\nconst { clientError, reconstructServiceError } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\n\n/** @typedef {import('dcp/utils').apiServiceType} apiServiceType */\n/** @typedef {import('dcp/utils').apiClientType} apiClientType */\n/** @typedef {string} opaqueId */\n\n/**\n * @typedef {object} cgAccessType\n * @property {opaqueId} [id]\n * @property {string} [joinKey]\n */\n\n/**\n * @typedef {object} cgClientJoinType\n * @property {opaqueId} [id]\n * @property {Address} [joinAddress]\n * @property {string} [joinKey]\n * @property {string} [joinSecret]\n * @property {string} [joinHash]\n */\n\n/**\n * @typedef {object} cgServiceJoinType\n * @property {opaqueId} [id]\n * @property {Address} [joinAddress]\n * @property {string} [joinKey]\n * @property {string} [joinHashHash]\n */\n\n/**\n * Establishes the client connection to the computeGroups microservice if it does not exist already from the default config.\n * \n * @returns {protocolV4.Connection}\n * @access public\n * @example\n * const result = await exports.serviceConnection.send('createGroup', {\n name: name,\n description: description,\n });\n */\n\nexports.serviceConnection = null;\n\n//\n// Reference counting pattern:\n// For every time addRef is called,\n// closeServiceConnection must eventually be called.\n// Reference counting allows multiple execs in a Promise.all .\n//\nvar refCount = 0;\nexports.addRef = function addRef() {\n refCount++;\n}\n\nconst openAndConnectServiceConn = async function openAndConnectServiceConn()\n{\n exports.serviceConnection = new protocolV4.Connection(dcpConfig.scheduler.services.computeGroups);\n exports.serviceConnection.on('close', openAndConnectServiceConn);\n await exports.serviceConnection.connect();\n refCount = 0; // Help with sanity.\n}\n\n/**\n * Resets the client connection to the computeGroups microservice.\n */\nexports.closeServiceConnection = async function closeServiceConnection() {\n if (refCount > 0) refCount--;\n if (exports.serviceConnection && refCount < 1)\n {\n exports.serviceConnection.off('close', openAndConnectServiceConn);\n exports.serviceConnection.close(null, true);\n refCount = 0; // Help with sanity.\n exports.serviceConnection = null;\n }\n};\n\n/**\n * (Used in jobs/index.js)\n * KeepAlive for the service connection to compute groups.\n */\nexports.keepAlive = async function keepAlive() {\n if (!exports.serviceConnection)\n await openAndConnectServiceConn();\n\n exports.serviceConnection.keepalive().catch(err => console.error('Warning: keepalive failed for compute groups service', err));\n}\n\n/**\n * Checks whether descriptor corresponds to the public compute group from the scheduler constants.\n */\nexports.isPublicComputeGroup = function isPublicComputeGroup(descriptor) {\n return descriptor.id === constants.computeGroups.public.id\n && descriptor.opaqueId === constants.computeGroups.public.opaqueId;\n};\n\n/**\n * Returns a compute group identification snippet for diagnostic messages,\n * @param {object} descriptor - Must have one of the properties joinKey, id (id:=opaqueId). Specifically\n * descriptor = { joinKey: 'dcpDemo' } or descriptor = { id: 'bYcYGQ3NOpFnP4FKs6IBQd' },\n * where the corresponding row in table computeGroups have attributes\n * joinKey:='dcpDemo' or opaqueId:='bYcYGQ3NOpFnP4FKs6IBQd' .\n * @returns {string}\n */\nfunction cgId(descriptor) {\n return (descriptor.joinKey) ? `joinKey ${descriptor.joinKey}` : `id ${descriptor.id}`;\n}\n\n/**\n * Verify sufficient information in descriptor to access a compute group.\n * Emit diagnostics about unnecessary information.\n * @param {cgAccessType} descriptor \n * @param {string} methodName \n */\nfunction validateCGDescriptor(descriptor, methodName) {\n for (const prop in descriptor) {\n if ([ 'id', 'joinKey' ].includes(prop)) continue;\n if ([ 'joinAddress', 'joinHash', 'joinSecret' ].includes(prop))\n console.warn(`It is not necessary to specify '${prop}' in the descriptor ${JSON.stringify(descriptor)} when calling ${methodName}`);\n else\n console.error(`Do not specify '${prop}' in the descriptor ${JSON.stringify(descriptor)} when calling ${methodName}`);\n }\n}\n\n/**\n * Verify sufficient information in descriptor to authorize a compute group.\n * Emit diagnostics about unnecessary information.\n * @param {cgClientJoinType} joinDescriptor \n * @param {string} methodName \n */\nfunction validateCGJoinDescriptor(joinDescriptor, methodName) {\n for (const prop in joinDescriptor) {\n if ([ 'id', 'joinKey', 'joinSecret', 'joinHash', 'joinAddress' ].includes(prop)) continue;\n console.error(`Do not specify '${prop}' in the descriptor ${JSON.stringify(joinDescriptor)} when calling ${methodName}`);\n }\n}\n\n/**\n * Build message to go across the wire.\n * Verify sufficient information in descriptor to access a compute group.\n * Emit diagnostics about unnecessary information.\n * @param {cgAccessType} descriptor\n * @param {string} methodName\n * @returns {cgAccessType}\n */\nfunction buildCGMessage(descriptor, methodName)\n{\n if (exports.isPublicComputeGroup(descriptor)) return descriptor;\n\n const message = {};\n // Construct message.joinKey xor message.id .\n if (descriptor.joinKey) message.joinKey = descriptor.joinKey;\n else if (descriptor.id) message.id = descriptor.id; // id:=opaqueId\n\n debugging('computeGroups') && console.debug(`${methodName}:buildCGMessage: descriptor`, descriptor, 'message', message);\n\n validateCGDescriptor(descriptor, methodName);\n\n return message;\n}\n\n/**\n * Build message so that joinHash, joinSecret, opaqueId do not go across the wire.\n * Verify sufficient information in descriptor to authorize a compute group.\n * Emit diagnostics about unnecessary information.\n * @param {cgClientJoinType} descriptor\n * @param {string} methodName\n * @returns {cgServiceJoinType}\n */\nfunction buildCGJoinMessage(descriptor, methodName)\n{\n if (exports.isPublicComputeGroup(descriptor)) return descriptor;\n\n const message = {};\n // Construct message.joinKey xor message.id .\n if (descriptor.joinKey) message.joinKey = descriptor.joinKey;\n else if (descriptor.id) message.id = descriptor.id; // id:=opaqueId\n // Construct message.joinAddress .\n if (descriptor.joinAddress) message.joinAddress = descriptor.joinAddress;\n\n debugging('computeGroups') && console.debug(`${methodName}:buildCGJoinMessage: descriptor`, descriptor, 'message', message);\n\n validateCGJoinDescriptor(descriptor, methodName);\n\n // Construct message.joinHashHash .\n if (descriptor.joinSecret) message.joinHashHash = hash.calculate(hash.eh1, exports.calculateJoinHash(descriptor), exports.serviceConnection.dcpsid);\n if (descriptor.joinHash) message.joinHashHash = hash.calculate(hash.eh1, descriptor.joinHash, exports.serviceConnection.dcpsid);\n\n return message;\n}\n\nfunction hasSufficientJoinInfo(joinDescriptor) {\n // Verify joinDescriptor has sufficient information to authorize a compute group (not guarenteed).\n return (joinDescriptor.joinKey && (joinDescriptor.joinSecret || joinDescriptor.joinHash))\n || (joinDescriptor.id && joinDescriptor.joinAddress)\n || exports.isPublicComputeGroup(joinDescriptor);\n}\n\nconst newCGPrototype = { type: 'object',\n parameters: {\n // name: { type: 'string', default: undefined }, /* name of group (length <= 255) */\n // description: { type: 'string', default: undefined }, /* description of group (length <= 255) */\n // id: { type: 'string', default: undefined }, /* opaqueId, the unique identifier of the compute group; nanoid (length === 22) */\n // joinKey: { type: 'string', default: undefined }, /* basically the login (length <= 255) */\n // joinSecret: { type: 'string', default: undefined }, /* basically the password (length <= 255) */\n // joinHash: { type: 'string', default: undefined }, /* basically the password, the joinSecret seeded & hashed */\n // joinAddress: { type: Address, default: undefined }, /* signature gives alternative to login/password */\n\n commissionRate: { type: 'BigNumber', default: undefined }, /* commission, see DCP-1889 */\n deployFee: { type: 'BigNumber', default: undefined }, /* number of DCC to take for every deployment */\n deployAccess: { type: 'string', default: undefined }, /* can be \"owner\"|\"join\" (dcp-1910) */\n addJobFee: { type: 'BigNumber', default: undefined }, /* fee required each time a job joins a compute group */\n maxTotalPayment: { type: 'BigNumber', default: undefined }, /* limit on maximum job payment, NULL => Infinity */\n\n /* Administrative limits on group. NULL => Infinity: Should all be integers or undefined. */\n maxConcurrentJobs: { type: 'number', default: undefined },\n maxConcurrentWorkers: { type: 'number', default: undefined },\n maxConcurrentSandboxes: { type: 'number', default: undefined },\n maxConcurrentCPUs: { type: 'number', default: undefined },\n maxConcurrentGPUs: { type: 'number', default: undefined },\n maxConcurrentEscrow: { type: 'BigNumber', default: undefined },\n },\n};\n\n/**\n * Async function that creates a new Compute Group.\n *\n * The joinDescriptor is of the form { joinKey, joinSecret }, { joinKey, joinHash } or { id, joinAddress }.\n * where id will correspond to the attribute opaqueId in the new row in the computeGroups table.\n *\n * This function can only be called with ADMIN permission.\n * Properties not appearing in newCGPrototype.parameters are not allowed in otherProperties.\n *\n * @param {cgClientJoinType} joinDescriptor - Must have properly defined { joinKey, joinSecret }, { joinKey, joinHash }\n * or { id, joinAddress }, where id will correspond to the attribute opaqueId\n * in the new row in the computeGroups table.\n * @param {string} [name] - The name of the compute group.\n * @param {string} [description] - The description of the compute group.\n * @param {object} [otherProperties] - The 5 attributes of table computeGroup related to commissions and fees.\n * commissionRate: notNull(zFinNum),// commission, see DCP-1889\n * deployFee: notNull(zFinNum),// number of DCC to take for every deployment\n * deployAccess: string, // can be \"owner\"|\"join\" (dcp-1910)\n * addJobFee: notNull(zFinNum),// fee required each time a job joins a compute group\n * maxTotalPayment: finNum, // limit on maximum job payment, NULL => Infinity\n * And the 6 attributes of table computeGroup related to limits.\n * maxConcurrentJobs: integer,\n * maxConcurrentWorkers: integer,\n * maxConcurrentSandboxes: integer,\n * maxConcurrentCPUs: integer,\n * maxConcurrentGPUs: integer,\n * maxConcurrentEscrow: finNum,\n * @returns {Promise<apiClientType>} - { success, payload: computeGroup.id }\n * @access public\n * @example\n * await computeGroup.createGroup({ joinKey: 'dcpDemo', joinSecret: 'theSecret' }, 'myCGName', 'myCGDescription', { deployFee: 0.00015 });\n * await computeGroup.createGroup({ joinKey: 'dcpDemo2', joinHash: 'eh1-e063976b20a92da97a27b9873465c6f2c9d6e4370befa86c8c1dd312c78befc2' });\n * await computeGroup.createGroup({ id: 'bYcYGQ3NOpFnP4FKs6IBQd', joinAddress: 'c15053fc30d4bdf91e2e0bba79578f8b649e55ea' });\n * where the corresponding rows in table computeGroups have attributes\n * row1: joinKey:='dcpDemo',\n * row2: joinKey:='dcpDemo2', joinHash:='eh1-e063976b20a92da97a27b9873465c6f2c9d6e4370befa86c8c1dd312c78befc2'\n * row3: opaqueId:='bYcYGQ3NOpFnP4FKs6IBQd', joinAddress:='c15053fc30d4bdf91e2e0bba79578f8b649e55ea' .\n */\nexports.createGroup = async function createGroup(joinDescriptor, name, description, otherProperties)\n{\n if (!exports.serviceConnection)\n await openAndConnectServiceConn();\n\n validateCGJoinDescriptor(joinDescriptor, 'createGroup');\n\n // Verify joinDescriptor has sufficient information to authorize a compute group (not guarenteed).\n if (!hasSufficientJoinInfo(joinDescriptor))\n return clientError(`createGroup: Insufficient information to authorize compute group: ${JSON.stringify(joinDescriptor)}.`);\n\n // Validate the properties in otherProperties.\n for (const methodName in otherProperties) {\n if (!Object.keys(newCGPrototype.parameters).includes(methodName))\n return clientError(`createGroup: Property ${methodName} cannot be speicfied in otherProperties. Can only specify ${JSON.stringify(Object.keys(newCGPrototype.parameters))}`);\n }\n\n // Translate joinSecret to joinHash.\n if (joinDescriptor.joinSecret) {\n joinDescriptor.joinHash = exports.calculateJoinHash(joinDescriptor);\n delete joinDescriptor.joinSecret;\n }\n\n if (otherProperties && (otherProperties.commissionRate < 0 || otherProperties.commissionRate >= 1))\n return clientError(`client-createGroup: commissionRate ${otherProperties.commissionRate} must be between 0 and 1 (0 <= commissionRate < 1).`);\n\n debugging('computeGroups') && console.debug('client-createGroup: input:', joinDescriptor, name, description, otherProperties);\n\n const { success, payload } = await exports.serviceConnection.send('createGroup', { joinDescriptor, name, description, otherProperties });\n\n if (!success) return clientError(`Cannot create new compute group, with ${cgId(joinDescriptor)}.`);\n if (payload && !payload.success) return reconstructServiceError(payload);\n\n debugging('computeGroups') && console.debug('client-createGroup: payload', payload);\n\n return payload;\n};\n\nconst changeCGPrototype = { type: 'object',\n parameters: {\n name: { type: 'string', default: undefined }, /* name of group (length <= 255) */\n description: { type: 'string', default: undefined }, /* description of group (length <= 255) */\n joinHash: { type: 'string', default: undefined }, /* basically the password, seeded & hashed (length <= 255) */\n joinAddress: { type: Address, default: undefined }, /* signature gives alternative to login/password */\n\n commissionRate: { type: 'BigNumber', default: undefined }, /* commission, see DCP-1889 */\n deployFee: { type: 'BigNumber', default: undefined }, /* number of DCC to take for every deployment */\n deployAccess: { type: 'string', default: undefined }, /* can be \"owner\"|\"join\" (dcp-1910) */\n addJobFee: { type: 'BigNumber', default: undefined }, /* fee required each time a job joins a compute group */\n maxTotalPayment: { type: 'BigNumber', default: undefined }, /* limit on maximum job payment, NULL => Infinity */\n\n /* Administrative limits on group. NULL => Infinity: Should all be integers or undefined. */\n maxConcurrentJobs: { type: 'number', default: undefined },\n maxConcurrentWorkers: { type: 'number', default: undefined },\n maxConcurrentSandboxes: { type: 'number', default: undefined },\n maxConcurrentCPUs: { type: 'number', default: undefined },\n maxConcurrentGPUs: { type: 'number', default: undefined },\n maxConcurrentEscrow: { type: 'BigNumber', default: undefined },\n },\n};\n\n/**\n * Async function that changes a new Compute Group.\n * \n * The parameter newDescriptor contains the new property values,\n * and the properties that are allowed to be changed appear in changeCGPrototype.parameters.\n * \n * The descriptor must have joinKey or id, where id:=opaqueId.\n * Must own the compute group or be ADMIN to use changeGroup.\n * \n * @param {cgAccessType} descriptor - Must have joinkey or id, where id:=opaqueId.\n * @param {object} newDescriptor - Properties not appearing in changeCGPrototype.parameters are not allowed.\n * @returns {Promise<apiClientType>}\n * await computeGroup.changeGroup({ joinKey: 'dcpDemo' }, { joinSecret: 'myNewPasswrd' });\n * await computeGroup.changeGroup({ id: 'bYcYGQ3NOpFnP4FKs6IBQd' }, { name: 'myNewName', deployFee: 0.0001 });\n * where the corresponding rows in table computeGroups have attributes\n * row1: joinKey:='dcpDemo',\n * row2: opaqueId:='bYcYGQ3NOpFnP4FKs6IBQd'\n */\nexports.changeGroup = async function changeGroup(descriptor, newDescriptor)\n{\n if (!exports.serviceConnection)\n await openAndConnectServiceConn();\n\n // Verify descriptor has sufficient information to access a compute group (not guarenteed).\n if (!descriptor.joinKey && !descriptor.id)\n return clientError(`changeGroup: Insufficient information to identify compute group: ${JSON.stringify(descriptor)}.`);\n\n // Validate the properties in newDescriptor.\n for (const methodName in newDescriptor) {\n if (!Object.keys(changeCGPrototype.parameters).includes(methodName))\n return clientError(`changeGroup: Not allowed to change property ${methodName}. Can only change ${JSON.stringify(Object.keys(changeCGPrototype.parameters))}`);\n }\n\n // Translate joinSecret to joinHash.\n if (newDescriptor.joinSecret) {\n newDescriptor.joinHash = exports.calculateJoinHash(newDescriptor);\n delete newDescriptor.joinSecret;\n }\n\n descriptor = buildCGMessage(descriptor, 'changeGroup');\n debugging('computeGroups') && console.debug('change compute group client:', descriptor, newDescriptor);\n const { success, payload } = await exports.serviceConnection.send('changeGroup', { descriptor, newDescriptor });\n\n if (!success) throw new DCPError(`Cannot change compute group with ${cgId(descriptor)}:`, payload);\n if (payload && !payload.success) return reconstructServiceError(payload);\n\n return payload;\n};\n\n/**\n * Async function that deletes a compute group.\n * \n * The descriptor must have joinkey or id, where id:=opaqueId.\n * \n * Must either own the group or be ADMIN.\n * If not ADMIN, then the following config must be true:\n * dcpConfig.scheduler.services.computeGroups.usersCanDeleteGroups\n * \n * @param {cgAccessType} descriptor - Must contain joinKey or id (id:=opaqueId) \n * @returns {Promise<apiClientType>}\n * await computeGroup.deleteGroup({ joinKey: 'dcpDemo' });\n * await computeGroup.deleteGroup({ id: 'bYcYGQ3NOpFnP4FKs6IBQd' });\n * where the corresponding rows in table computeGroups have attributes\n * row1: joinKey:='dcpDemo',\n * row2: opaqueId:='bYcYGQ3NOpFnP4FKs6IBQd'\n */\nexports.deleteGroup = async function deleteGroup(descriptor)\n{\n if (!exports.serviceConnection)\n await openAndConnectServiceConn();\n\n // Verify descriptor has sufficient information to access a compute group (not guarenteed).\n if (!descriptor.joinKey && !descriptor.id)\n return clientError(`deleteGroup: Insufficient information to identify compute group: ${JSON.stringify(descriptor)}.`);\n\n descriptor = buildCGMessage(descriptor, 'deleteGroup');\n debugging('computeGroups') && console.debug('delete compute group client:', descriptor);\n const { success, payload } = await exports.serviceConnection.send('deleteGroup', { descriptor });\n\n if (!success) throw new DCPError(`Cannot delete compute group with ${cgId(descriptor)}:`, payload);\n if (payload && !payload.success) return reconstructServiceError(payload);\n\n return payload;\n};\n\n/**\n * Async function that adds a job to a specified compute group. \n * \n * Must be the owner of the job.\n *\n * Useful feedback is provided from this function, as it\n * will make its way back to the application developer, *after* they have made the\n * deployment fee micropayment.\n *\n * On the client side the access model in place is that if you know the (user/password)\n * joinKey+joinSecret/joinKey+joinHash/joinKey+joinHashHash/id+joinAddress,\n * you can add the job to the compute groups, where id:=opaqueId from table computeGroups.\n * On the service side the corresponding access model is\n * joinKey+joinHashHash/id+joinAddress .\n * Access is also allowed if the compute group owner is the connection peerAddress.\n * \n * Unless the compute group owner is the connection peerAddress, element of the descriptor array must contain\n * { joinKey, joinSecret }, { joinKey, joinHash } or { id, joinAddress }\n * where the value of id in { id, joinAddress } is the opaqueId attribute of the row in table computeGroups.\n *\n * @param {Address} job The address of the Job that will be added to the Compute Group.\n * @param {Array} computeGroups Array of descriptor objects for the compute groups. This descriptor\n * needs to contain enough information to authorize access to the\n * compute group. Properties may include:\n * - id (id:=opaqueId)\n * - joinKey\n * - joinSecret\n * - joinHash\n * - joinAddress\n * \n * Additional, either the joinKey or id MUST be specified so\n * that we can identify the compute group in question.\n *\n * All compute groups can have jobs submitted to them, provided either the joinKey\n * or the id are specified, and the message contains valid join permission and the \n * job is owned by the caller of addJobToGroups.\n *\n * FUTURE - after DCP-1910\n * keystore A keystore used to grant access to job deployment within this compute group.\n * This can be either the ownerKeystore or the joinAddress keystore when the\n * compute group is in deployAccessType='join' mode.\n * @returns {Promise<apiClientType>}\n * @access public\n * @example\n * await computeGroup.addJobToGroups('P+Y4IApeFQLrYS2W7MkVg7', \n * [ { joinKey: 'dcpDemo', joinSecret: 'theSecret' },\n * { joinKey: 'dcpDemo2', joinHash: 'eh1-e063976b20a92da97a27b9873465c6f2c9d6e4370befa86c8c1dd312c78befc2' }, \n * { id: 'bYcYGQ3NOpFnP4FKs6IBQd', joinAddress: 'c15053fc30d4bdf91e2e0bba79578f8b649e55ea' } ]);\n * where the corresponding rows in table computeGroups have attributes\n * row1: joinKey:='dcpDemo',\n * row2: joinKey:='dcpDemo2', joinHash:='eh1-e063976b20a92da97a27b9873465c6f2c9d6e4370befa86c8c1dd312c78befc2'\n * row3: opaqueId:='bYcYGQ3NOpFnP4FKs6IBQd', joinAddress:='c15053fc30d4bdf91e2e0bba79578f8b649e55ea' .\n */\nexports.addJobToGroups = async function addJobToGroups(job, computeGroups)\n{\n // ensure the service connection is hot before calling buildCGJoinMessage:\n await exports.keepAlive();\n\n const cgArray = [];\n for (const joinDescriptor of computeGroups)\n {\n // Verify joinDescriptor has sufficient information to authorize a compute group (not guarenteed).\n if (!hasSufficientJoinInfo(joinDescriptor))\n {\n console.error(`addJobToGroups: Insufficient information to authorize compute group: ${JSON.stringify(joinDescriptor)}.`);\n continue;\n }\n\n // Translate so that neither joinHash nor joinSecret goes across the wire.\n const message = buildCGJoinMessage(joinDescriptor, 'addJobToGroups');\n debugging('computeGroups') && console.debug(`addJobToGroups client: job ${job}, message`, message);\n\n cgArray.push(message);\n }\n\n const { success, payload } = await exports.serviceConnection.send('addJobToGroups', { job, cgArray });\n\n debugging('computeGroups') && console.debug('addJobToGroups payload', payload);\n\n if (!success) throw new DCPError(`Cannot add job ${job} to compute groups.`, payload);\n if (payload && !payload.success) return reconstructServiceError(payload);\n\n // If the server reported success but did not return a list of CGs (eg. v.4.2.5 server),\n // assume (and inform the client) we added all groups successfully\n return payload || computeGroups;\n};\n\n/**\n * Async function that lists all the Jobs in a Compute Group.\n * \n * The descriptor must have one of the properties joinkey, id (id:=opaqueId).\n * Must be the owner of the Compute Group to list jobs from it.\n * The job does not need to be owned.\n * \n * The descriptor is of the form { joinKey: 'dcpDemo' } or { id: 'bYcYGQ3NOpFnP4FKs6IBQd' }.\n * where 'bYcYGQ3NOpFnP4FKs6IBQd' is the opaqueId of the Compute Group.\n *\n * @param {cgAccessType} descriptor - Must have one of the properties joinKey, id (id:=opaqueId). Specifically\n * descriptor = { joinKey: 'dcpDemo' } or descriptor = { id: opaqueId }\n * @returns {Promise<apiClientType>}\n * @access public\n * @example\n * let listOfJobs1 = await computeGroup.listJobs({ joinKey: 'dcpDemo' });\n * let listOfJobs2 = await computeGroup.listJobs({ id: 'bYcYGQ3NOpFnP4FKs6IBQd' });\n * where the corresponding rows in table computeGroups have attributes\n * row1: joinKey:='dcpDemo'\n * row2: opaqueId:='bYcYGQ3NOpFnP4FKs6IBQd'\n */\nexports.listJobs = async function listJobs(descriptor)\n{\n if (!exports.serviceConnection)\n await openAndConnectServiceConn();\n\n // Verify descriptor has sufficient information to access a compute group (not guarenteed).\n if (!descriptor.joinKey && !descriptor.id)\n return clientError(`listJobs: Insufficient information to identify compute group: ${JSON.stringify(descriptor)}.`);\n\n descriptor = buildCGMessage(descriptor, 'listJobs');\n debugging('computeGroups') && console.debug('listJob client: descriptor', descriptor);\n const { success, payload } = await exports.serviceConnection.send('listJobs', { descriptor });\n\n if (!success) throw new DCPError(`Cannot list jobs for compute group with ${cgId(descriptor)}`, payload);\n if (payload && !payload.success) return reconstructServiceError(payload);\n\n return payload;\n};\n\n/**\n * Async function that removes a job from a Compute Group.\n * \n * The descriptor must have one of the properties joinkey, id (id:=opaqueId).\n * Must be the owner of the Compute Group to remove a job from it.\n * The job does not need to be owned.\n * \n * The descriptor is of the form { joinKey: 'dcpDemo' } or { id: 'bYcYGQ3NOpFnP4FKs6IBQd' }.\n * where 'bYcYGQ3NOpFnP4FKs6IBQd' is the opaqueId of the Compute Group.\n *\n * @param {Address} job - The address of the Job that will be added to the Compute Group.\n * @param {cgAccessType} descriptor - { joinKey: 'dcpDemo' } or { id: 'bYcYGQ3NOpFnP4FKs6IBQd' }\n * @returns {Promise<apiClientType>}\n * @access public\n * @example\n * await computeGroup.removeJob( 'P+Y4IApeFQLrYS2W7MkVg7', { joinKey: 'dcpDemo' });\n * await computeGroup.removeJob( 'P+Y4IApeFQLrYS2W7MkVg7', { id: 'bYcYGQ3NOpFnP4FKs6IBQd' });\n * where the corresponding rows in table computeGroups have attributes\n * row1: joinKey:='dcpDemo'\n * row2: opaqueId:='bYcYGQ3NOpFnP4FKs6IBQd'\n */\nexports.removeJob = async function removeJob(job, descriptor)\n{\n if (!exports.serviceConnection)\n await openAndConnectServiceConn();\n\n // Verify descriptor has sufficient information to access a compute group (not guarenteed).\n if (!descriptor.joinKey && !descriptor.id)\n return clientError(`removeJob: Insufficient information to identify compute group: ${JSON.stringify(descriptor)}.`);\n\n descriptor = buildCGMessage(descriptor, 'removeJob');\n debugging('computeGroups') && console.debug(`removeJob client: job ${job}, descriptor`, descriptor);\n const { success, payload } = await exports.serviceConnection.send('removeJob', { job, descriptor });\n\n if (!success) throw new DCPError(`Cannot remove job ${job} from compute group with ${cgId(descriptor)}`, payload);\n if (payload && !payload.success) return reconstructServiceError(payload);\n\n return payload;\n};\n\n/**\n * Async function that removes all jobs from a Compute Group.\n * \n * The descriptor must have one of the properties joinkey, id (id:=opaqueId).\n * Must be the owner of the Compute Group to remove jobs from it.\n * \n * The descriptor is of the form { joinKey: 'dcpDemo' } or { id: 'bYcYGQ3NOpFnP4FKs6IBQd' }.\n * where 'bYcYGQ3NOpFnP4FKs6IBQd' is the opaqueId of the Compute Group.\n *\n * @param {cgAccessType} descriptor - { joinKey: 'dcpDemo' } or { id: 'bYcYGQ3NOpFnP4FKs6IBQd' }\n * @returns {Promise<apiClientType>}\n * @access public\n * @example\n * await computeGroup.removeAllJobs({ joinKey: 'dcpDemo' });\n * await computeGroup.removeAllJobs({ id: 'bYcYGQ3NOpFnP4FKs6IBQd' });\n * where the corresponding rows in table computeGroups have attributes\n * row1: joinKey:='dcpDemo'\n * row2: opaqueId:='bYcYGQ3NOpFnP4FKs6IBQd'\n */\nexports.removeAllJobs = async function removeAllJobs(descriptor)\n{\n if (!exports.serviceConnection)\n await openAndConnectServiceConn();\n\n // Verify descriptor has sufficient information to access a compute group (not guarenteed).\n if (!descriptor.joinKey && !descriptor.id)\n return clientError(`removeAllJobs: Insufficient information to identify compute group: ${JSON.stringify(descriptor)}.`);\n\n descriptor = buildCGMessage(descriptor, 'removeAllJobs');\n debugging('computeGroups') && console.debug('removeAllJobs client: descriptor', descriptor);\n const { success, payload } = await exports.serviceConnection.send('removeAllJobs', { descriptor });\n\n if (!success) throw new DCPError(`Cannot remove all jobs from compute group with ${cgId(descriptor)}:`, payload);\n if (payload && !payload.success) return reconstructServiceError(payload);\n\n return payload;\n};\n\n/**\n * Async function that cancels the specified owned job.\n * \n * Must be the owner of the job.\n *\n * On the client side the access model in place is that if you know the (user/password)\n * joinKey+joinSecret/joinKey+joinHash/joinKey+joinHashHash/id+joinAddress,\n * you can cancel the job in the compute group, where id:=opaqueId from table computeGroups.\n * On the service side the corresponding access model is\n * joinKey+joinHashHash/id+joinAddress .\n * Access is also allowed if the compute group owner is the connection peerAddress.\n * \n * Unless the compute group owner is the connection peerAddress, the descriptor must contain\n * { joinKey, joinHashHash } or { id, joinAddress }\n * where the value of id in { id, joinAddress } is the opaqueId attribute of the row in table computeGroups.\n * \n * @param {Address} job - The address of the Job that will be added to the Compute Group.\n * @param {cgClientJoinType} joinDescriptor - Array of descriptor objects for the compute groups. This descriptor\n * needs to contain enough information to authorize access to the\n * compute group. Properties may include:\n * - id (id:=opaqueId)\n * - joinKey\n * - joinSecret\n * - joinHash\n * - joinAddress\n *\n * Additional, either the joinKey or id MUST be specified so\n * that we can identify the compute group in question.\n * @returns {Promise<apiClientType>}\n * @access public\n * @example\n * await computeGroup.cancelJob( 'P+Y4IApeFQLrYS2W7MkVg7', { joinKey: 'dcpDemo', joinHash: 'eh1-e063976b20a92da97a27b9873465c6f2c9d6e4370befa86c8c1dd312c78befc2' } );\n * await computeGroup.cancelJob( 'P+Y4IApeFQLrYS2W7MkVg7', { id: 'bYcYGQ3NOpFnP4FKs6IBQd', joinAddress: 'c15053fc30d4bdf91e2e0bba79578f8b649e55ea' });\n * where the corresponding rows in table computeGroups have attributes\n * row1: joinKey:='dcpDemo', joinHash:='eh1-e063976b20a92da97a27b9873465c6f2c9d6e4370befa86c8c1dd312c78befc2'\n * row2: opaqueId:='bYcYGQ3NOpFnP4FKs6IBQd', joinAddress:='c15053fc30d4bdf91e2e0bba79578f8b649e55ea' .\n */\nexports.cancelJob = async function cancelJob(job, joinDescriptor)\n{\n if (!exports.serviceConnection)\n await openAndConnectServiceConn();\n\n // Verify joinDescriptor has sufficient information to authorize a compute group (not guarenteed).\n if (!hasSufficientJoinInfo(joinDescriptor))\n return clientError(`cancelJob: Insufficient information to authorize compute group: ${JSON.stringify(joinDescriptor)}.`);\n\n // Translate so that neither joinHash nor joinSecret goes across the wire.\n joinDescriptor = buildCGJoinMessage(joinDescriptor, 'cancelJob');\n debugging('computeGroups') && console.debug(`cancelJob client: job ${job}, descriptor`, joinDescriptor);\n const { success, payload } = await exports.serviceConnection.send('cancelJob', { job, joinDescriptor });\n\n if (!success) throw new DCPError(`Cannot cancel job ${job} for compute group with ${cgId(joinDescriptor)}:`, payload);\n if (payload && !payload.success) return reconstructServiceError(payload);\n\n return payload;\n};\n\n/**\n * Async function that cancels the owned job in the Compute Group.\n * \n * On the client side the access model in place is that if you know the (user/password)\n * joinKey+joinSecret/joinKey+joinHash/joinKey+joinHashHash/id+joinAddress,\n * you can cancel the jobs in the compute group, where id:=opaqueId from table computeGroups.\n * On the service side the corresponding access model is\n * joinKey+joinHashHash/id+joinAddress .\n * Access is also allowed if the compute group owner is the connection peerAddress.\n * \n * Unless the compute group owner is the connection peerAddress, the descriptor must contain\n * { joinKey, joinHashHash } or { id, joinAddress }\n * where the value of id in { id, joinAddress } is the opaqueId attribute of the row in table computeGroups.\n * \n * @param {cgClientJoinType} joinDescriptor - Array of descriptor objects for the compute groups. This descriptor\n * needs to contain enough information to authorize access to the\n * compute group. Properties may include:\n * - id (id:=opaqueId)\n * - joinKey\n * - joinSecret\n * - joinHash\n * - joinAddress\n * \n * Additional, either the joinKey or id MUST be specified so\n * that we can identify the compute group in question.\n * @returns {Promise<apiClientType>}\n * @access public\n * @example\n * await computeGroup.cancelAllJobs( { joinKey: 'dcpDemo', joinHash: 'eh1-e063976b20a92da97a27b9873465c6f2c9d6e4370befa86c8c1dd312c78befc2' } );\n * await computeGroup.cancelAllJobs( { id: 'bYcYGQ3NOpFnP4FKs6IBQd', joinAddress: 'c15053fc30d4bdf91e2e0bba79578f8b649e55ea' });\n * where the corresponding rows in table computeGroups have attributes\n * row1: joinKey:='dcpDemo', joinHash:='eh1-e063976b20a92da97a27b9873465c6f2c9d6e4370befa86c8c1dd312c78befc2'\n * row2: opaqueId:='bYcYGQ3NOpFnP4FKs6IBQd', joinAddress:='c15053fc30d4bdf91e2e0bba79578f8b649e55ea' .\n */\nexports.cancelAllJobs = async function cancelAllJobs(joinDescriptor)\n{\n if (!exports.serviceConnection)\n await openAndConnectServiceConn();\n\n // Verify joinDescriptor has sufficient information to authorize a compute group (not guarenteed).\n if (!hasSufficientJoinInfo(joinDescriptor))\n return clientError(`cancelAllJobs: Insufficient information to authorize compute group: ${JSON.stringify(joinDescriptor)}.`);\n\n // Translate so that neither joinHash nor joinSecret goes across the wire.\n joinDescriptor = buildCGJoinMessage(joinDescriptor, 'cancelAllJobs');\n debugging('computeGroups') && console.debug('cancelAllJobs client: descriptor', joinDescriptor);\n const { success, payload } = await exports.serviceConnection.send('cancelAllJobs', { joinDescriptor });\n\n if (!success) throw new DCPError(`Cannot cancel owned jobs for compute group with ${cgId(joinDescriptor)}:`, payload);\n if (payload && !payload.success) return reconstructServiceError(payload);\n\n return payload;\n};\n\n/**\n * Calculate a joinHash for a compute group. This is an eh1- hash of the cg salt and \n * joinSecret components of a compute group description.\n *\n * @param {object} details an object containing the cg salt, which is\n * the joinKey if the compute group uses one;\n * otherwise it is the joinAddress. This object\n * may also contain the joinSecret.\n * @param {string} [joinSecret] the join secret -- plain text -- that is\n * the \"password\" for the compute group. If not\n * specified, we use details.joinSecret.\n */\nexports.calculateJoinHash = function computeGroups$calculateJoinHash(details, joinSecret)\n{\n if (typeof joinSecret === 'undefined')\n joinSecret = details.joinSecret;\n\n return hash.calculate(hash.eh1, `${details.joinKey || details.joinAddress} ${joinSecret}`);\n}\n\n\n//# sourceURL=webpack://dcp/./src/dcp-client/compute-groups/index.js?");
|
|
4162
4162
|
|
|
4163
4163
|
/***/ }),
|
|
4164
4164
|
|
|
@@ -4168,7 +4168,7 @@ eval("/**\n * @file Client facing module that implements Compute Groups API\n
|
|
|
4168
4168
|
\***********************************/
|
|
4169
4169
|
/***/ ((__unused_webpack_module, exports, __webpack_require__) => {
|
|
4170
4170
|
|
|
4171
|
-
eval("/**\n * @file Module that implements Compute API\n * @module dcp/compute\n * @access public\n */\n\n/* global dcpConfig */\n\n/**\n * @access private - Will change soon!\n * @typedef {object} JobRequirements\n * @property {object} environment\n * @property {boolean} environment.fdlibm - Workers express capability 'fdlibm' in order for client applications to have confidence that results will be bitwise identical across workers. This library is recommended, but not required, for implementaiton of Google chrome, Mozilla Firefox and the DCP standalone worker use fdlibm but Microsoft Edge and Apple Safari do not use it.\n * @property {boolean} environment.offscreenCanvas When present, this capability indicates that the worker environment has the OffscreenCanvas symbol defined.\n * @property {object} details\n * @property {object} details.offscreenCanvas\n * @property {boolean} details.offscreenCanvas.bigTexture4096 This capability indicates that the worker's WebGL MAX_TEXTURE_SIZE is at least 4096.\n * @property {boolean} details.offscreenCanvas.bigTexture8192 This capability indicates that the worker's WebGL MAX_TEXTURE_SIZE is at least 8192.\n * @property {boolean} details.offscreenCanvas.bigTexture16384 This capability indicates that the worker's WebGL MAX_TEXTURE_SIZE is at least 16384.\n * @property {boolean} details.offscreenCanvas.bigTexture32768 This capability indicates that the worker's WebGL MAX_TEXTURE_SIZE is at least 32768.\n * @property {object} engine\n * @property {boolean} engine.es7 This capability enforces that the worker is running an es7-compliant JavaScript engine.\n * @property {boolean} engine.spidermonkey The capability enforces that the worker will run in the SpiderMonkey JS engine.\n * @property {boolean} browser.chrome The capability to exclude Chrome browsers.\n */\n\n/**\n * This JSDoc 'namespace' is a convenient way to document some globals that are available only in\n * the sandbox environment provided to work functions.\n * @access public\n * @namespace sandboxEnv\n */\n\n/**\n * This function emits a progress event. Progress events should be emitted approximately once per second; a task which fails to emit a \n * progress event for a certain period of time will be cancelled by the supervisor. The argument to this function is interpreted to six \n * significant digits, and must increase for every call. *All work functions must emit at least one progress event* - this requirement \n * will be enforced by the estimator. * The period of time mentioned above will be at least 30 wall-clock seconds and at least 30 \n * benchmark-adjusted seconds\n * @access public\n * @method progress\n * @param {string|number|undefined} n a number between 0 and 1 (inclusive) that represents a best-guess \n * at the completed portion of the task as a ratio of completed work to total work. If the argument is a string ending in the `%` symbol, \n * it will be interpreted as a number in the usual mathematical sense. If the argument is `undefined`, the slice progress is noted but \n * the amount of progress is considered to be indeterminate.\n * @memberof module:dcp/compute~sandboxEnv\n * @returns {boolean} true\n */\n\n/**\n * Emit a `console` event to the `JobHandle` in the client. If the sandbox is running in an environment with a native console object, the native method may also be invoked.\n * \n * @member console\n * @property {function} log fire console event with 'log' as `level` (see {@link Job#event:console|Job#event:console.level}).\n * @property {function} debug fire console event with 'debug' as `level` (see {@link Job#event:console|Job#event:console.level}).\n * @property {function} info fire console event with 'info' as `level` (see {@link Job#event:console|Job#event:console.level}).\n * @property {function} warn fire console event with 'warn' as `level` (see {@link Job#event:console|Job#event:console.level}).\n * @property {function} error fire console event with 'error' as `level` (see {@link Job#event:console|Job#event:console.level}).\n * \n * **Note** some ES environments (Chrome, Firefox) implement C-style print formatting in this method. This is currently not supported.\n * @memberof module:dcp/compute~sandboxEnv\n * @emits Job#console\n * @access public\n */\n\n/**\n * @member work An object that contains the following properties\n * @property {function} emit (eventName, ...) - send arbitrary args with `eventName`. This event is received client-side with those args (see {@link Job#work}).\n * @property {object} job\n * @property {object} job.public Property set on job handle, see {@link Job#public}.\n * @memberof module:dcp/compute~sandboxEnv\n * @access public\n */\n\n/**\n * @member {OffscreenCanvas} OffscreenCanvas As defined in the {@link https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvas|HTML Standard}, provides a canvas which can be rendered off-screen. If this interface is not available in a given worker, the worker will not report capability \"offscreenCanvas\".\n * @memberof module:dcp/compute~sandboxEnv\n * @access public\n */\n\nconst protocolV4 = __webpack_require__(/*! dcp/protocol-v4 */ \"./src/protocol-v4/index.js\");\nconst { EventEmitter } = __webpack_require__(/*! dcp/common/dcp-events/event-emitter */ \"./src/common/dcp-events/event-emitter.js\");\nconst { RangeObject, rehydrateRange } = __webpack_require__(/*! dcp/dcp-client/range-object */ \"./src/dcp-client/range-object.js\");\nconst { RemoteDataSet } = __webpack_require__(/*! dcp/dcp-client/remote-data-set */ \"./src/dcp-client/remote-data-set.js\");\nconst { RemoteDataPattern } = __webpack_require__(/*! dcp/dcp-client/remote-data-pattern */ \"./src/dcp-client/remote-data-pattern.js\");\nconst { Job, ResultHandle } = __webpack_require__(/*! dcp/dcp-client/job */ \"./src/dcp-client/job/index.js\");\nconst { jobStatuses } = __webpack_require__(/*! dcp/common/scheduler-constants */ \"./src/common/scheduler-constants.js\");\nconst wallet = __webpack_require__(/*! dcp/dcp-client/wallet */ \"./src/dcp-client/wallet/index.js\");\nconst { DcpURL } = __webpack_require__(/*! dcp/common/dcp-url */ \"./src/common/dcp-url.js\");\nconst { disableWorker } = __webpack_require__(/*! dcp/dcp-client/worker */ \"./src/dcp-client/worker/index.js\");\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\nconst debugging = (__webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope)('dcp-client');\n\nconst debug = (...args) => {\n if (debugging()) {\n args.unshift('dcp-client:compute');\n console.debug(...args);\n }\n};\n\nconst MARKET_VALUE = Symbol('Market Value');\nconst indirectEval = eval; // eslint-disable-line no-eval\n\n/**\n * Instance methods of this class are exported as static members of {@link module:dcp/compute|dcp/compute}.\n * @example\n * let job = compute.for(1, 3, function (i) {\n * progress('100%');\n * return i*10;\n * })\n * let results = await job.exec();\n */\nclass Compute extends EventEmitter {\n constructor () {\n super('Compute');\n\n /** @todo move these into some central config */\n this.defaultMaxGPUs = 1;\n this.GPUsAssigned = 0;\n this.allowSlicesToFinishBeforeStopping = false;\n\n this.schedulerURL = undefined; // Will default to dcpConfig.scheduler.location if not overidden\n this.bankURL = undefined; // Will default to dcpConfig.bank.services.bankTeller.location if not overidden\n\n Object.defineProperty(this.marketValue, MARKET_VALUE, {\n value: true,\n writable: false,\n configurable: false,\n enumerable: false,\n });\n\n const ceci = this;\n this.RemoteDataSet = RemoteDataSet;\n this.RemoteDataPattern = RemoteDataPattern;\n // Initialize to null so these properties are recognized for the Job class\n this.phemeConnection = null;\n this.phemeUses = 0;\n this.deployConnection = null;\n this.openPhemeConn = function openPhemeConn()\n {\n ceci.phemeConnection = new protocolV4.Connection(dcpConfig.scheduler.services.pheme);\n ceci.phemeConnection.on('close', ceci.openPhemeConn);\n }\n\n this.openDeployConn = function openDeployConn()\n {\n ceci.deployConnection = new protocolV4.Connection(dcpConfig.scheduler.services.jobSubmit);\n ceci.deployConnection.on('close', ceci.openDeployConn);\n }\n }\n\n /** 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 instanceof RangeObject || range instanceof Function) ? range : (Array.isArray(range) ? range.length + ' elements' : '<yes>'));\n debug('Arguments:', Array.isArray(genArgs) && genArgs.length + ' elements');\n return new Job(work, range, genArgs);\n\n /**\n * @param {any} workFunction\n */\n function isValidWorkFunctionType(workFunction) {\n return (\n typeof workFunction === 'function' ||\n typeof workFunction === 'string' ||\n DcpURL.isURL(workFunction)\n );\n }\n\n /**\n * Checks if the workFunction can be evaluated to a function or if it's\n * protocol is HTTPS,\n * @param {function | string | URL | DcpURL} workFunction\n */\n function isWorkFunctionValid(workFunction) {\n switch (typeof workFunction) {\n case 'function':\n return true;\n case 'string': {\n return isValidFunction(workFunction);\n }\n case 'object':\n return DcpURL.isURL(workFunction);\n default:\n return false;\n }\n }\n\n /**\n * Evaluates to true if the string can be coerced into a javascript\n * function.\n * @param {string} workFunction\n */\n function isValidFunction(workFunction) {\n try {\n const fn = indirectEval(`(${workFunction})`);\n return typeof fn === 'function';\n } catch (e) {\n return false;\n }\n }\n }\n\n /**\n * Form 1.\n * Create a job which will run a work function once\n *\n * @param {function | string | URL | DcpURL} work Work function as string or function literal.\n * @param {object} [workFunctionArgs=[]] optional Array-like object which contains arguments which are passed to the work func\n * @returns {Job}\n * @access public\n * @example\n * const job = compute.do(function (i) {\n * progress('100%')\n * return i*10\n * })\n */\n /**\n * Form 2.\n * Create a job which will run a work function on the values 1..n\n *\n * @param {number} n Number of times to run the work function.\n * @param {function | string | URL | DcpURL} work Work function as string or function literal.\n * @param {object} [workFunctionArgs=[]] optional Array-like object which contains arguments which are passed to the work func\n * @returns {Job}\n * @access public\n * @example\n * const job = compute.do(5, function (i) {\n * progress('100%')\n * return i*10\n * })\n */\n do(...args) {\n let n;\n let work;\n let workFunctionArgs;\n if (typeof args[0] !== 'number') {\n // Form 1.\n n = 1;\n [work, workFunctionArgs] = args;\n } else {\n // Form 2.\n [n, work, workFunctionArgs] = args;\n }\n return this.for(0, n - 1, work, workFunctionArgs);\n }\n\n /** Cancel a running job, by opaque id\n *\n * @param {string} id Address of the job to cancel.\n * @param {wallet.Keystore} ownerKeystore The keystore of the job owner\n * @param {string} reason If provided, will be sent on to client\n * @returns {object} The status of the funds that were released from escrow.\n * @throws when id or ownerKeystore are not provided.\n * @access public\n */\n async cancel (id, ownerKeystore, reason = '') {\n if (typeof id !== 'string') {\n throw new Error(`compute.cancel: Job ID must be a string, not ${typeof id}.`);\n }\n\n if (!(ownerKeystore instanceof wallet.Keystore)) {\n throw new Error('compute.cancel: ownerKeystore must be instance of wallet.Keystore');\n }\n\n if (!this.deployConnection)\n this.openDeployConn();\n\n const response = await this.deployConnection.send('cancelJob', {\n job: id,\n owner: ownerKeystore.address,\n reason,\n }, ownerKeystore);\n\n this.deployConnection.off('close', this.deployConnection)\n await this.deployConnection.close();\n\n return response.payload;\n }\n\n /**\n * Reconnect/resume a job, by opaque id\n *\n * @param {string} id Opaque id of the Job to resume/reconnect.\n * @param {wallet.Keystore} [ownerKeystore=wallet.get()] The keystore of the job owner\n * @returns {Job}\n * @throws when id is not provided.\n * @access public\n */\n async resume (id, ownerKeystore) {\n if (typeof id !== 'string') {\n throw new Error(`compute.resume: Job ID must be a string, not ${typeof id}.`);\n }\n\n if (!(ownerKeystore instanceof wallet.Keystore)) {\n ownerKeystore = await wallet.get();\n }\n\n const response = await this.schedulerConnection.send('fetchJob', {\n job: id, // XXX@TODO change 'address' to 'job' once we port this operation to v4\n owner: ownerKeystore.address,\n }, ownerKeystore);\n\n const job = new Job(response.payload);\n job.setPaymentAccountKeystore(ownerKeystore);\n return job;\n }\n\n /**\n * Form 1. Query the status of a job that has been deployed to the scheduler.\n *\n * @access public\n * @param {string|Job} job A job ID or a Job instance\n * @throws when ownerKeystore is not provided\n * @returns {Promise<object>} A status object describing a given job.\n */\n /**\n * Form 2. Query the status of jobs deployed to the scheduler.\n *\n * @access public\n * @param {object} [opts] Query options\n * @param {number} [opts.startTime] Jobs older than this will be excluded from\n * the results.\n * @param {number} [opts.endTime] Jobs newer than this will be excluded from\n * the results.\n * @throws when ownerKeystore is not provided\n * @returns {Promise<Array<Object>>} An array of status objects corresponding to the status of\n * jobs deployed by the given payment account.\n */\n async status(...args) {\n const requestPayload = {};\n\n const firstArg = args.shift();\n if (typeof firstArg === 'string' || firstArg instanceof Job) {\n // Form 1\n requestPayload.jobAddress = firstArg instanceof Job ? firstArg.id : firstArg;\n requestPayload.statuses = jobStatuses;\n } else if (typeof firstArg === 'object') {\n // Form 2\n if (\n (typeof firstArg.startTime !== 'undefined' &&\n typeof firstArg.startTime !== 'number' &&\n !(firstArg.startTime instanceof Date)) ||\n (typeof firstArg.endTime !== 'undefined' &&\n typeof firstArg.endTime !== 'number' &&\n !(firstArg.endTime instanceof Date))\n ) {\n throw new TypeError(\n 'startTime and or endTime were not specified as instances of Date or milliseconds since epoch',\n );\n }\n\n requestPayload.startDeployTime = firstArg.startTime;\n requestPayload.endDeployTime = firstArg.endTime;\n \n if (typeof firstArg.statuses !== 'undefined'\n && (!(firstArg.statuses instanceof Array)\n || !(firstArg.statuses.every((status) => jobStatuses.includes(status))))\n ) {\n throw new TypeError(`Job statuses must be specified in an Array and can only include ${jobStatuses}`)\n }\n \n requestPayload.statuses = firstArg.statuses || jobStatuses;\n \n } else {\n throw new TypeError(`Unknown first argument type ${typeof firstArg}`);\n }\n\n this.phemeUses++;\n if (!this.phemeConnection)\n this.openPhemeConn();\n\n const {\n success,\n payload: responsePayload,\n } = await this.phemeConnection.send(\n 'listJobs',\n requestPayload\n );\n\n if (!success) {\n throw new DCPError('Request for job status failed', responsePayload);\n }\n\n if (this.phemeConnection && this.phemeUses === 1)\n {\n this.phemeConnection.off('close', this.openPhemeConn);\n this.phemeConnection.close();\n this.phemeConnection = null;\n }\n this.phemeUses--;\n\n if (responsePayload.length > 0 && (typeof firstArg === 'string' || firstArg instanceof Job)) {\n return responsePayload[0];\n }\n\n return responsePayload;\n }\n\n /**\n * Returns information for the job with the provided ID. It will use\n * `wallet.getId()` to retrieve the auth keystore.\n *\n * @param {string} jobAddress\n * @returns {Promise<object>} status object describing a given job\n * @access public\n */\n async getJobInfo(jobAddress) {\n this.phemeUses++;\n if (!this.phemeConnection)\n this.openPhemeConn();\n\n const ks = await wallet.getId();\n const { payload: jobResponsePayload } = await this.phemeConnection.send('fetchJobReport', \n {\n jobOwner: ks.address,\n job: jobAddress\n });\n\n const { payload: sliceResponsePayload } = await this.phemeConnection.send(\n 'fetchSliceReport', \n {\n jobOwner: ks.address,\n job: jobAddress\n },\n );\n\n const jobReport = {};\n Object.assign(jobReport, jobResponsePayload, sliceResponsePayload);\n\n if (this.phemeConnection && this.phemeUses === 1)\n {\n this.phemeConnection.off('close', this.openPhemeConn);\n this.phemeConnection.close();\n this.phemeConnection = null;\n }\n this.phemeUses--;\n\n return jobReport;\n }\n\n /**\n * Returns information for the slices of the job with the provided ID. It will\n * use `wallet.getId()` to retrieve the auth keystore.\n *\n * @param {string} jobAddress\n * @returns {Promise<object>} A slice status object for the given job.\n * @access public\n */\n async getSliceInfo(jobAddress) {\n this.phemeUses++;\n if (!this.phemeConnection)\n this.openPhemeConn();\n\n const identityKeystore = await wallet.getId();\n const {\n success,\n payload: responsePayload,\n } = await this.phemeConnection.send(\n 'fetchSliceReport',\n {\n job: jobAddress,\n jobOwner: identityKeystore.address,\n },\n identityKeystore,\n );\n\n if (!success) {\n throw new DCPError('Request for slice info failed', responsePayload);\n }\n\n if (this.phemeConnection && this.phemeUses === 1)\n {\n this.phemeConnection.off('close', this.openPhemeConn)\n this.phemeConnection.close();\n this.phemeConnection = null;\n }\n this.phemeUses--;\n\n return responsePayload;\n }\n}\n\nexports.RemoteDataSet = RemoteDataSet;\nexports.RemoteDataPattern = RemoteDataPattern;\nexports.MARKET_VALUE = MARKET_VALUE;\nexports.compute = new Compute(); /* :( */\nexports.compute.getPublicComputeGroupId = () => (__webpack_require__(/*! dcp/common/scheduler-constants */ \"./src/common/scheduler-constants.js\").computeGroups[\"public\"].id);\nexports.compute.getPublicComputeGroupDescriptor = () => (__webpack_require__(/*! dcp/common/scheduler-constants */ \"./src/common/scheduler-constants.js\").computeGroups[\"public\"]);\nexports.compute.ResultHandle = ResultHandle; // export as compute.ResultHandle for instanceof checking\nexports.compute.Job = Job;\nexports.version = {\n api: '1.5.2',\n provides: '1.0.0' /* dcpConfig.scheduler.compatibility.operations.compute */\n};\n\n\n//# sourceURL=webpack://dcp/./src/dcp-client/compute.js?");
|
|
4171
|
+
eval("/**\n * @file Module that implements Compute API\n * @module dcp/compute\n * @access public\n */\n\n/* global dcpConfig */\n\n/**\n * @access private - Will change soon!\n * @typedef {object} JobRequirements\n * @property {object} environment\n * @property {boolean} environment.fdlibm - Workers express capability 'fdlibm' in order for client applications to have confidence that results will be bitwise identical across workers. This library is recommended, but not required, for implementaiton of Google chrome, Mozilla Firefox and the DCP standalone worker use fdlibm but Microsoft Edge and Apple Safari do not use it.\n * @property {boolean} environment.offscreenCanvas When present, this capability indicates that the worker environment has the OffscreenCanvas symbol defined.\n * @property {object} details\n * @property {object} details.offscreenCanvas\n * @property {boolean} details.offscreenCanvas.bigTexture4096 This capability indicates that the worker's WebGL MAX_TEXTURE_SIZE is at least 4096.\n * @property {boolean} details.offscreenCanvas.bigTexture8192 This capability indicates that the worker's WebGL MAX_TEXTURE_SIZE is at least 8192.\n * @property {boolean} details.offscreenCanvas.bigTexture16384 This capability indicates that the worker's WebGL MAX_TEXTURE_SIZE is at least 16384.\n * @property {boolean} details.offscreenCanvas.bigTexture32768 This capability indicates that the worker's WebGL MAX_TEXTURE_SIZE is at least 32768.\n * @property {object} engine\n * @property {boolean} engine.es7 This capability enforces that the worker is running an es7-compliant JavaScript engine.\n * @property {boolean} engine.spidermonkey The capability enforces that the worker will run in the SpiderMonkey JS engine.\n * @property {boolean} browser.chrome The capability to exclude Chrome browsers.\n */\n\n/**\n * This JSDoc 'namespace' is a convenient way to document some globals that are available only in\n * the sandbox environment provided to work functions.\n * @access public\n * @namespace sandboxEnv\n */\n\n/**\n * This function emits a progress event. Progress events should be emitted approximately once per second; a task which fails to emit a \n * progress event for a certain period of time will be cancelled by the supervisor. The argument to this function is interpreted to six \n * significant digits, and must increase for every call. *All work functions must emit at least one progress event* - this requirement \n * will be enforced by the estimator. * The period of time mentioned above will be at least 30 wall-clock seconds and at least 30 \n * benchmark-adjusted seconds\n * @access public\n * @method progress\n * @param {string|number|undefined} n a number between 0 and 1 (inclusive) that represents a best-guess \n * at the completed portion of the task as a ratio of completed work to total work. If the argument is a string ending in the `%` symbol, \n * it will be interpreted as a number in the usual mathematical sense. If the argument is `undefined`, the slice progress is noted but \n * the amount of progress is considered to be indeterminate.\n * @memberof module:dcp/compute~sandboxEnv\n * @returns {boolean} true\n */\n\n/**\n * Emit a `console` event to the `JobHandle` in the client. If the sandbox is running in an environment with a native console object, the native method may also be invoked.\n * \n * @member console\n * @property {function} log fire console event with 'log' as `level` (see {@link Job#event:console|Job#event:console.level}).\n * @property {function} debug fire console event with 'debug' as `level` (see {@link Job#event:console|Job#event:console.level}).\n * @property {function} info fire console event with 'info' as `level` (see {@link Job#event:console|Job#event:console.level}).\n * @property {function} warn fire console event with 'warn' as `level` (see {@link Job#event:console|Job#event:console.level}).\n * @property {function} error fire console event with 'error' as `level` (see {@link Job#event:console|Job#event:console.level}).\n * \n * **Note** some ES environments (Chrome, Firefox) implement C-style print formatting in this method. This is currently not supported.\n * @memberof module:dcp/compute~sandboxEnv\n * @emits Job#console\n * @access public\n */\n\n/**\n * @member work An object that contains the following properties\n * @property {function} emit (eventName, ...) - send arbitrary args with `eventName`. This event is received client-side with those args (see {@link Job#work}).\n * @property {object} job\n * @property {object} job.public Property set on job handle, see {@link Job#public}.\n * @memberof module:dcp/compute~sandboxEnv\n * @access public\n */\n\n/**\n * @member {OffscreenCanvas} OffscreenCanvas As defined in the {@link https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvas|HTML Standard}, provides a canvas which can be rendered off-screen. If this interface is not available in a given worker, the worker will not report capability \"offscreenCanvas\".\n * @memberof module:dcp/compute~sandboxEnv\n * @access public\n */\n\nconst protocolV4 = __webpack_require__(/*! dcp/protocol-v4 */ \"./src/protocol-v4/index.js\");\nconst { EventEmitter } = __webpack_require__(/*! dcp/common/dcp-events/event-emitter */ \"./src/common/dcp-events/event-emitter.js\");\nconst { RangeObject, rehydrateRange } = __webpack_require__(/*! dcp/dcp-client/range-object */ \"./src/dcp-client/range-object.js\");\nconst { RemoteDataSet } = __webpack_require__(/*! dcp/dcp-client/remote-data-set */ \"./src/dcp-client/remote-data-set.js\");\nconst { RemoteDataPattern } = __webpack_require__(/*! dcp/dcp-client/remote-data-pattern */ \"./src/dcp-client/remote-data-pattern.js\");\nconst { Job, ResultHandle } = __webpack_require__(/*! dcp/dcp-client/job */ \"./src/dcp-client/job/index.js\");\nconst { jobStatuses } = __webpack_require__(/*! dcp/common/scheduler-constants */ \"./src/common/scheduler-constants.js\");\nconst wallet = __webpack_require__(/*! dcp/dcp-client/wallet */ \"./src/dcp-client/wallet/index.js\");\nconst { DcpURL } = __webpack_require__(/*! dcp/common/dcp-url */ \"./src/common/dcp-url.js\");\nconst { 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 /** 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 * 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 instanceof RangeObject || range instanceof Function) ? range : (Array.isArray(range) ? range.length + ' elements' : '<yes>'));\n debug('Arguments:', Array.isArray(genArgs) && genArgs.length + ' elements');\n return new Job(work, range, genArgs);\n\n /**\n * @param {any} workFunction\n */\n function isValidWorkFunctionType(workFunction) {\n return (\n typeof workFunction === 'function' ||\n typeof workFunction === 'string' ||\n DcpURL.isURL(workFunction)\n );\n }\n\n /**\n * Checks if the workFunction can be evaluated to a function or if it's\n * protocol is HTTPS,\n * @param {function | string | URL | DcpURL} workFunction\n */\n function isWorkFunctionValid(workFunction) {\n switch (typeof workFunction) {\n case 'function':\n return true;\n case 'string': {\n return isValidFunction(workFunction);\n }\n case 'object':\n return DcpURL.isURL(workFunction);\n default:\n return false;\n }\n }\n\n /**\n * Evaluates to true if the string can be coerced into a javascript\n * function.\n * @param {string} workFunction\n */\n function isValidFunction(workFunction) {\n try {\n const fn = indirectEval(`(${workFunction})`);\n return typeof fn === 'function';\n } catch (e) {\n return false;\n }\n }\n }\n\n /**\n * Form 1.\n * Create a job which will run a work function once\n *\n * @param {function | string | URL | DcpURL} work Work function as string or function literal.\n * @param {object} [workFunctionArgs=[]] optional Array-like object which contains arguments which are passed to the work func\n * @returns {Job}\n * @access public\n * @example\n * const job = compute.do(function (i) {\n * progress('100%')\n * return i*10\n * })\n */\n /**\n * Form 2.\n * Create a job which will run a work function on the values 1..n\n *\n * @param {number} n Number of times to run the work function.\n * @param {function | string | URL | DcpURL} work Work function as string or function literal.\n * @param {object} [workFunctionArgs=[]] optional Array-like object which contains arguments which are passed to the work func\n * @returns {Job}\n * @access public\n * @example\n * const job = compute.do(5, function (i) {\n * progress('100%')\n * return i*10\n * })\n */\n do(...args) {\n let n;\n let work;\n let workFunctionArgs;\n if (typeof args[0] !== 'number') {\n // Form 1.\n n = 1;\n [work, workFunctionArgs] = args;\n } else {\n // Form 2.\n [n, work, workFunctionArgs] = args;\n }\n return this.for(0, n - 1, work, workFunctionArgs);\n }\n\n /** Cancel a running job, by opaque id\n *\n * @param {string} id Address of the job to cancel.\n * @param {wallet.Keystore} ownerKeystore The keystore of the job owner\n * @param {string} reason If provided, will be sent on to client\n * @returns {object} The status of the funds that were released from escrow.\n * @throws when id or ownerKeystore are not provided.\n * @access public\n */\n async cancel (id, ownerKeystore, reason = '') {\n if (typeof id !== 'string') {\n throw new Error(`compute.cancel: Job ID must be a string, not ${typeof id}.`);\n }\n\n if (!(ownerKeystore instanceof wallet.Keystore)) {\n throw new Error('compute.cancel: ownerKeystore must be instance of wallet.Keystore');\n }\n\n if (!this.deployConnection)\n this.openDeployConn();\n\n const response = await this.deployConnection.send('cancelJob', {\n job: id,\n owner: ownerKeystore.address,\n reason,\n }, ownerKeystore);\n\n this.deployConnection.off('close', this.deployConnection)\n await this.deployConnection.close();\n\n return response.payload;\n }\n\n /**\n * Reconnect/resume a job, by opaque id\n *\n * @param {string} id Opaque id of the Job to resume/reconnect.\n * @param {wallet.Keystore} [ownerKeystore=wallet.get()] The keystore of the job owner\n * @returns {Job}\n * @throws when id is not provided.\n * @access public\n */\n async resume (id, ownerKeystore) {\n if (typeof id !== 'string') {\n throw new Error(`compute.resume: Job ID must be a string, not ${typeof id}.`);\n }\n\n if (!(ownerKeystore instanceof wallet.Keystore)) {\n ownerKeystore = await wallet.get();\n }\n\n const response = await this.schedulerConnection.send('fetchJob', {\n job: id, // XXX@TODO change 'address' to 'job' once we port this operation to v4\n owner: ownerKeystore.address,\n }, ownerKeystore);\n\n const job = new Job(response.payload);\n job.setPaymentAccountKeystore(ownerKeystore);\n return job;\n }\n\n /**\n * Form 1. Query the status of a job that has been deployed to the scheduler.\n *\n * @access public\n * @param {string|Job} job A job ID or a Job instance\n * @throws when ownerKeystore is not provided\n * @returns {Promise<object>} A status object describing a given job.\n */\n /**\n * Form 2. Query the status of jobs deployed to the scheduler.\n *\n * @access public\n * @param {object} [opts] Query options\n * @param {number} [opts.startTime] Jobs older than this will be excluded from\n * the results.\n * @param {number} [opts.endTime] Jobs newer than this will be excluded from\n * the results.\n * @throws when ownerKeystore is not provided\n * @returns {Promise<Array<Object>>} An array of status objects corresponding to the status of\n * jobs deployed by the given payment account.\n */\n async status(...args) {\n const requestPayload = {};\n\n const firstArg = args.shift();\n if (typeof firstArg === 'string' || firstArg instanceof Job) {\n // Form 1\n requestPayload.jobAddress = firstArg instanceof Job ? firstArg.id : firstArg;\n requestPayload.statuses = jobStatuses;\n } else if (typeof firstArg === 'object') {\n // Form 2\n if (\n (typeof firstArg.startTime !== 'undefined' &&\n typeof firstArg.startTime !== 'number' &&\n !(firstArg.startTime instanceof Date)) ||\n (typeof firstArg.endTime !== 'undefined' &&\n typeof firstArg.endTime !== 'number' &&\n !(firstArg.endTime instanceof Date))\n ) {\n throw new TypeError(\n 'startTime and or endTime were not specified as instances of Date or milliseconds since epoch',\n );\n }\n\n requestPayload.startDeployTime = firstArg.startTime;\n requestPayload.endDeployTime = firstArg.endTime;\n \n if (typeof firstArg.statuses !== 'undefined'\n && (!(firstArg.statuses instanceof Array)\n || !(firstArg.statuses.every((status) => jobStatuses.includes(status))))\n ) {\n throw new TypeError(`Job statuses must be specified in an Array and can only include ${jobStatuses}`)\n }\n \n requestPayload.statuses = firstArg.statuses || jobStatuses;\n \n } else {\n throw new TypeError(`Unknown first argument type ${typeof firstArg}`);\n }\n\n this.phemeUses++;\n if (!this.phemeConnection)\n this.openPhemeConn();\n\n const {\n success,\n payload: responsePayload,\n } = await this.phemeConnection.send(\n 'listJobs',\n requestPayload\n );\n\n if (!success) {\n throw new DCPError('Request for job status failed', responsePayload);\n }\n\n if (this.phemeConnection && this.phemeUses === 1)\n {\n this.phemeConnection.off('close', this.openPhemeConn);\n this.phemeConnection.close();\n this.phemeConnection = null;\n }\n this.phemeUses--;\n\n if (responsePayload.length > 0 && (typeof firstArg === 'string' || firstArg instanceof Job)) {\n return responsePayload[0];\n }\n\n return responsePayload;\n }\n\n /**\n * Returns information for the job with the provided ID. It will use\n * `wallet.getId()` to retrieve the auth keystore.\n *\n * @param {string} jobAddress\n * @returns {Promise<object>} status object describing a given job\n * @access public\n */\n async getJobInfo(jobAddress) {\n this.phemeUses++;\n if (!this.phemeConnection)\n this.openPhemeConn();\n\n const ks = await wallet.getId();\n const { payload: jobResponsePayload } = await this.phemeConnection.send('fetchJobReport', \n {\n jobOwner: ks.address,\n job: jobAddress\n });\n\n const { payload: sliceResponsePayload } = await this.phemeConnection.send(\n 'fetchSliceReport', \n {\n jobOwner: ks.address,\n job: jobAddress\n },\n );\n\n const jobReport = {};\n Object.assign(jobReport, jobResponsePayload, sliceResponsePayload);\n\n if (this.phemeConnection && this.phemeUses === 1)\n {\n this.phemeConnection.off('close', this.openPhemeConn);\n this.phemeConnection.close();\n this.phemeConnection = null;\n }\n this.phemeUses--;\n\n return jobReport;\n }\n\n /**\n * Returns information for the slices of the job with the provided ID. It will\n * use `wallet.getId()` to retrieve the auth keystore.\n *\n * @param {string} jobAddress\n * @returns {Promise<object>} A slice status object for the given job.\n * @access public\n */\n async getSliceInfo(jobAddress) {\n this.phemeUses++;\n if (!this.phemeConnection)\n this.openPhemeConn();\n\n const identityKeystore = await wallet.getId();\n const {\n success,\n payload: responsePayload,\n } = await this.phemeConnection.send(\n 'fetchSliceReport',\n {\n job: jobAddress,\n jobOwner: identityKeystore.address,\n },\n identityKeystore,\n );\n\n if (!success) {\n throw new DCPError('Request for slice info failed', responsePayload);\n }\n\n if (this.phemeConnection && this.phemeUses === 1)\n {\n this.phemeConnection.off('close', this.openPhemeConn)\n this.phemeConnection.close();\n this.phemeConnection = null;\n }\n this.phemeUses--;\n\n return responsePayload;\n }\n}\n\nexports.RemoteDataSet = RemoteDataSet;\nexports.RemoteDataPattern = RemoteDataPattern;\nexports.MARKET_VALUE = MARKET_VALUE;\nexports.compute = new Compute(); /* :( */\nexports.compute.getPublicComputeGroupId = () => (__webpack_require__(/*! dcp/common/scheduler-constants */ \"./src/common/scheduler-constants.js\").computeGroups[\"public\"].id);\nexports.compute.getPublicComputeGroupDescriptor = () => (__webpack_require__(/*! dcp/common/scheduler-constants */ \"./src/common/scheduler-constants.js\").computeGroups[\"public\"]);\nexports.compute.ResultHandle = ResultHandle; // export as compute.ResultHandle for instanceof checking\nexports.compute.Job = Job;\nexports.version = {\n api: '1.5.2',\n provides: '1.0.0' /* dcpConfig.scheduler.compatibility.operations.compute */\n};\n\n\n//# sourceURL=webpack://dcp/./src/dcp-client/compute.js?");
|
|
4172
4172
|
|
|
4173
4173
|
/***/ }),
|
|
4174
4174
|
|
|
@@ -4178,7 +4178,7 @@ eval("/**\n * @file Module that implements Compute API\n * @module dcp/comput
|
|
|
4178
4178
|
\*********************************/
|
|
4179
4179
|
/***/ ((module, exports, __webpack_require__) => {
|
|
4180
4180
|
|
|
4181
|
-
eval("/* module decorator */ module = __webpack_require__.nmd(module);\n/**\n * @file dcp-client-bundle-src.js\n * Top-level file which gets webpacked into the bundle consumed by dcp-client 2.5\n * @author Wes Garland, wes@kingsds.network\n * @date July 2019\n */\n\n{\n let thisScript = typeof document !== 'undefined' ? (typeof document.currentScript !== 'undefined' && document.currentScript) || document.getElementById('_dcp_client_bundle') : {}\n let realModuleDeclare\n\n if ( false || typeof module.declare === 'undefined') {\n realModuleDeclare = ( true) ? module.declare : 0\n if (false) {}\n module.declare = function moduleUnWrapper (deps, factory) {\n factory(null, module.exports, module)\n return module.exports\n }\n }\n\n let _debugging = () => false\n dcpConfig.future = (__webpack_require__(/*! ../common/config-future.js */ \"./src/common/config-future.js\").futureFactory)(_debugging, dcpConfig);\n\n /* These modules are official API and must be part of DCP Client */\n let officialApi = {\n 'protocol': __webpack_require__(/*! ../protocol-v4 */ \"./src/protocol-v4/index.js\"),\n 'compute': (__webpack_require__(/*! ./compute */ \"./src/dcp-client/compute.js\").compute),\n 'worker': __webpack_require__(/*! ./worker */ \"./src/dcp-client/worker/index.js\"),\n 'wallet': __webpack_require__(/*! ./wallet */ \"./src/dcp-client/wallet/index.js\"),\n };\n\n /* Allow client programs to use modules which happen to be in the bundle anyhow */\n let conveniencePeers = {\n 'ethereumjs-wallet': (__webpack_require__(/*! ./wallet/keystore */ \"./src/dcp-client/wallet/keystore.js\")._internalEth.wallet),\n 'ethereumjs-util': (__webpack_require__(/*! ./wallet/keystore */ \"./src/dcp-client/wallet/keystore.js\")._internalEth.util),\n 'socket.io-client': __webpack_require__(/*! socket.io-client */ \"./node_modules/socket.io-client/build/cjs/index.js\"),\n 'bignumber.js': __webpack_require__(/*! bignumber.js */ \"./node_modules/bignumber.js/bignumber.js\"),\n 'semver': __webpack_require__(/*! semver */ \"./node_modules/semver/semver.js\"),\n };\n\n /* Some of these modules are API-track. Some of them need to be published to be\n * available for top-level resolution by DCP internals. Those (mostly) should have\n * been written using relative module paths.....\n */\n let modules = Object.assign({\n 'dcp-build': {\"version\":\"
|
|
4181
|
+
eval("/* module decorator */ module = __webpack_require__.nmd(module);\n/**\n * @file dcp-client-bundle-src.js\n * Top-level file which gets webpacked into the bundle consumed by dcp-client 2.5\n * @author Wes Garland, wes@kingsds.network\n * @date July 2019\n */\n\n{\n let thisScript = typeof document !== 'undefined' ? (typeof document.currentScript !== 'undefined' && document.currentScript) || document.getElementById('_dcp_client_bundle') : {}\n let realModuleDeclare\n\n if ( false || typeof module.declare === 'undefined') {\n realModuleDeclare = ( true) ? module.declare : 0\n if (false) {}\n module.declare = function moduleUnWrapper (deps, factory) {\n factory(null, module.exports, module)\n return module.exports\n }\n }\n\n let _debugging = () => false\n dcpConfig.future = (__webpack_require__(/*! ../common/config-future.js */ \"./src/common/config-future.js\").futureFactory)(_debugging, dcpConfig);\n\n /* These modules are official API and must be part of DCP Client */\n let officialApi = {\n 'protocol': __webpack_require__(/*! ../protocol-v4 */ \"./src/protocol-v4/index.js\"),\n 'compute': (__webpack_require__(/*! ./compute */ \"./src/dcp-client/compute.js\").compute),\n 'worker': __webpack_require__(/*! ./worker */ \"./src/dcp-client/worker/index.js\"),\n 'wallet': __webpack_require__(/*! ./wallet */ \"./src/dcp-client/wallet/index.js\"),\n };\n\n /* Allow client programs to use modules which happen to be in the bundle anyhow */\n let conveniencePeers = {\n 'ethereumjs-wallet': (__webpack_require__(/*! ./wallet/keystore */ \"./src/dcp-client/wallet/keystore.js\")._internalEth.wallet),\n 'ethereumjs-util': (__webpack_require__(/*! ./wallet/keystore */ \"./src/dcp-client/wallet/keystore.js\")._internalEth.util),\n 'socket.io-client': __webpack_require__(/*! socket.io-client */ \"./node_modules/socket.io-client/build/cjs/index.js\"),\n 'bignumber.js': __webpack_require__(/*! bignumber.js */ \"./node_modules/bignumber.js/bignumber.js\"),\n 'semver': __webpack_require__(/*! semver */ \"./node_modules/semver/semver.js\"),\n };\n\n /* Some of these modules are API-track. Some of them need to be published to be\n * available for top-level resolution by DCP internals. Those (mostly) should have\n * been written using relative module paths.....\n */\n let modules = Object.assign({\n 'dcp-build': {\"version\":\"bcdd0728ce28e9f58c7093651f0113bf0221c93a\",\"branch\":\"release\",\"dcpClient\":{\"version\":\"4.2.17\",\"from\":\"git+ssh://git@gitlab.com/Distributed-Compute-Protocol/dcp-client.git#prod-20221004\",\"resolved\":\"git+ssh://git@gitlab.com/Distributed-Compute-Protocol/dcp-client.git#9b75e977dc0b144e2e2e42ee9d12e063e46754f0\"},\"built\":\"Wed Oct 05 2022 10:04:37 GMT-0400 (Eastern Daylight Saving Time)\",\"config\":{\"generated\":\"Wed 05 Oct 2022 10:04:35 AM EDT by erose on lorge\",\"build\":\"debug\"},\"webpack\":\"5.70.0\",\"node\":\"v14.20.1\"},\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 'job-values': __webpack_require__(/*! ./job-values */ \"./src/dcp-client/job-values.js\"),\n 'standard-objects': {}\n }, conveniencePeers, officialApi);\n\n /* Export the JS Standard Classes (etc) from the global object of the bundle evaluation context,\n * in case we have code somewhere that needs to use these for instanceof checks.\n */\n ;[ Object, Function, Boolean, Symbol,\n Error, EvalError, RangeError, ReferenceError, SyntaxError, TypeError, URIError,\n Number, Math, Date,\n String, RegExp,\n Array, Int8Array, Uint8Array, Uint8ClampedArray, Int16Array, Uint16Array, Int32Array, Uint32Array, Float32Array, Float64Array,\n Map, Set, WeakMap, WeakSet,\n ArrayBuffer, DataView, JSON,\n Promise, \n Reflect, Proxy, Intl, WebAssembly, __webpack_require__\n ].forEach(function (obj) {\n if (obj.name && (typeof obj === 'function' || typeof obj === 'object'))\n modules['standard-objects'][obj.name] = obj\n })\n\n if (typeof BigInt !== 'undefined')\n modules['standard-objects']['BigInt'] === BigInt;\n if (typeof BigInt64Array !== 'undefined')\n modules['standard-objects']['BigInt64Array'] === BigInt64Array;\n if (typeof BigInt64Array !== 'undefined')\n modules['standard-objects']['BigUint64Array'] === BigUint64Array;\n\n module.declare([], function(require, exports, module) {\n Object.assign(exports, modules)\n exports['dcp-config'] = dcpConfig\n })\n if (realModuleDeclare)\n module.declare = realModuleDeclare\n\n bundleExports = thisScript.exports = exports; /* must be last expression evaluated! */\n}\n\n\n//# sourceURL=webpack://dcp/./src/dcp-client/index.js?");
|
|
4182
4182
|
|
|
4183
4183
|
/***/ }),
|
|
4184
4184
|
|
|
@@ -4188,7 +4188,7 @@ eval("/* module decorator */ module = __webpack_require__.nmd(module);\n/**\n *
|
|
|
4188
4188
|
\**************************************/
|
|
4189
4189
|
/***/ ((__unused_webpack_module, exports, __webpack_require__) => {
|
|
4190
4190
|
|
|
4191
|
-
eval("/**\n * @file job-values.js\n * Utility code related to encoding or decode client-supplied values that are used in the\n * worker (or vice-versa), such as elements of the input set, output set, and work function\n * arguments.\n * @author Wes Garland, wes@kingsds.network\n * @date Feb 2022\n */\n\nconst { DcpURL } = __webpack_require__(/*! dcp/common/dcp-url */ \"./src/common/dcp-url.js\");\nconst { encodeDataURI } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\nconst KVIN = __webpack_require__(/*! kvin */ \"./node_modules/kvin/kvin.js\");\n\n/** @todo - move submodules into a dir; refactor to use this module only */\nObject.assign({}, __webpack_require__(/*! ./remote-data-set */ \"./src/dcp-client/remote-data-set.js\"));\nObject.assign({}, __webpack_require__(/*! ./remote-data-pattern */ \"./src/dcp-client/remote-data-pattern.js\"));\n\n/**\n * Represent a single datum at a \"remote\" location.\n *\n * @constructor
|
|
4191
|
+
eval("/**\n * @file job-values.js\n * Utility code related to encoding or decode client-supplied values that are used in the\n * worker (or vice-versa), such as elements of the input set, output set, and work function\n * arguments.\n * @author Wes Garland, wes@kingsds.network\n * @date Feb 2022\n */\n\nconst { DcpURL } = __webpack_require__(/*! dcp/common/dcp-url */ \"./src/common/dcp-url.js\");\nconst { encodeDataURI } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\nconst KVIN = __webpack_require__(/*! kvin */ \"./node_modules/kvin/kvin.js\");\n\n/** @todo - move submodules into a dir; refactor to use this module only */\nObject.assign({}, __webpack_require__(/*! ./remote-data-set */ \"./src/dcp-client/remote-data-set.js\"));\nObject.assign({}, __webpack_require__(/*! ./remote-data-pattern */ \"./src/dcp-client/remote-data-pattern.js\"));\n\n/**\n * Represent a single datum at a \"remote\" location.\n *\n * @constructor RemoteValue\n * @extends DcpURL\n */\nexports.RemoteValue = function remoteDataSet$$RemoteValue()\n{\n DcpURL.apply(this, arguments);\n}\nexports.RemoteValue.prototype = new DcpURL('http://remote.value.dcp/prototype');\n\n/**\n * Serialize a job value, detecting the correct serialization form and MIME type; \n * JSON and KVIN selected as appropriate.\n *\n * @param {any} value The value to serialize\n * @returns {object} with three properties: string, method and MIMEType. String is the\n * serialized version of the value, and method tells us how it was serialized; \n * eg. kvin or json.\n */\nexports.serializeJobValue = function jobValues$$serializeJobValue(value)\n{\n var string;\n var methodFn;\n\n switch (typeof value) {\n default:\n methodFn = KVIN;\n break;\n case 'string':\n case 'boolean':\n methodFn = JSON;\n break;\n case 'number':\n if (!isNaN(value) && value !== Infinity && value !== -Infinity && value !== -0)\n methodFn = JSON;\n else\n methodFn = KVIN;\n break;\n case 'object':\n if (typeof value.toJSON === 'function')\n methodFn = JSON;\n else\n methodFn = KVIN;\n break;\n }\n\n string = methodFn.stringify(value);\n\n const method = methodFn === JSON ? 'json' : 'kvin';\n const MIMEType = exports.mimeTypes_bySerializer[method];\n return { string, method, MIMEType };\n}\n\nexports.mimeTypes_bySerializer = {\n 'kvin': 'application/x-kvin',\n 'json': 'application/json',\n};\n\n/**\n * Encode a job value for storage in the database, transmission to a worker, etc.\n * \n * @param {any} value the value to encode\n * @returns {string} a data: URI representing the value\n */\nexports.encodeJobValueUri = function jobValues$$encodeJobValueUri(value)\n{\n if (DcpURL.isURL(value))\n return value;\n \n const { string, MIMEType } = exports.serializeJobValue(value);\n return encodeDataURI(string, MIMEType);\n}\n\n\n//# sourceURL=webpack://dcp/./src/dcp-client/job-values.js?");
|
|
4192
4192
|
|
|
4193
4193
|
/***/ }),
|
|
4194
4194
|
|
|
@@ -4199,7 +4199,7 @@ eval("/**\n * @file job-values.js\n * Utility code related t
|
|
|
4199
4199
|
/***/ ((__unused_webpack_module, exports, __webpack_require__) => {
|
|
4200
4200
|
|
|
4201
4201
|
"use strict";
|
|
4202
|
-
eval("/**\n * @file job/index.js\n * @author Eddie Roosenmaallen, eddie@kingsds.network\n * Matthew Palma, mpalma@kingsds.network\n * Wes Garland, wes@kingsds.network\n * Paul, paul@kingsds.network\n * Ryan Saweczko, ryansaweczko@kingsds.network\n * @date November 2018\n * November 2018\n * February 2022\n * May 2022\n * Jun 2022\n *\n * This module implements the Compute API's Job Handle\n *\n */\n\n\nconst { BigNumber } = __webpack_require__(/*! bignumber.js */ \"./node_modules/bignumber.js/bignumber.js\");\nconst { v4: uuidv4 } = __webpack_require__(/*! uuid */ \"./node_modules/uuid/dist/esm-browser/index.js\");\nconst { EventEmitter, PropagatingEventEmitter } = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\");\nconst { RangeObject, MultiRangeObject, DistributionRange, SuperRangeObject, SparseRangeObject } = __webpack_require__(/*! dcp/dcp-client/range-object */ \"./src/dcp-client/range-object.js\");\nconst { fetchURI, encodeDataURI, createTempFile } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\nconst { encodeJobValueUri, RemoteValue } = __webpack_require__(/*! dcp/dcp-client/job-values */ \"./src/dcp-client/job-values.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 { DcpURL } = __webpack_require__(/*! dcp/common/dcp-url */ \"./src/common/dcp-url.js\");\nconst ClientModal = __webpack_require__(/*! dcp/dcp-client/client-modal */ \"./src/dcp-client/client-modal/index.js\");\nconst { Worker } = __webpack_require__(/*! dcp/dcp-client/worker */ \"./src/dcp-client/worker/index.js\");\nconst { RemoteDataSet } = __webpack_require__(/*! dcp/dcp-client/remote-data-set */ \"./src/dcp-client/remote-data-set.js\");\nconst { RemoteDataPattern } = __webpack_require__(/*! dcp/dcp-client/remote-data-pattern */ \"./src/dcp-client/remote-data-pattern.js\");\nconst { ResultHandle } = __webpack_require__(/*! ./result-handle */ \"./src/dcp-client/job/result-handle.js\");\nconst { SlicePaymentOffer } = __webpack_require__(/*! ./slice-payment-offer */ \"./src/dcp-client/job/slice-payment-offer.js\");\nconst { addSlices } = __webpack_require__(/*! ./upload-slices */ \"./src/dcp-client/job/upload-slices.js\");\nconst DCP_ENV = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\nconst 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 { DEFAULT_REQUIREMENTS, removeBadRequirements } = __webpack_require__(/*! dcp/common/job-requirements-defaults */ \"./src/common/job-requirements-defaults.js\");\nconst { sliceStatus, jobValueKind } = __webpack_require__(/*! dcp/common/scheduler-constants */ \"./src/common/scheduler-constants.js\");\nconst { jobStatus } = __webpack_require__(/*! dcp/common/scheduler-constants */ \"./src/common/scheduler-constants.js\");\nconst bankUtil = __webpack_require__(/*! dcp/dcp-client/bank-util */ \"./src/dcp-client/bank-util.js\");\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\nconst debugging = (__webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope)('dcp-client');\nconst kvin = __webpack_require__(/*! kvin */ \"./node_modules/kvin/kvin.js\");\nlet tunedKvin;\n\nconst log = (...args) => {\n if (debugging('job')) {\n console.debug('dcp-client:job', ...args);\n }\n};\n\nconst ON_BROWSER = DCP_ENV.isBrowserPlatform;\nconst sideloaderModuleIdentifier = 'sideloader-v1';\n\n\n/** @typedef {import('dcp/dcp-client/wallet/keystore').Keystore} Keystore */\n/** @typedef {import('dcp/dcp-client/range-object').RangeLike} RangeLike */\n/** @typedef {import('scheduler-v4/libexec/job-submit/operations/submit').MarshaledInputData} MarshaledInputData */\n\n/**\n * Ensure input data is an appropriate format\n * @param {RangeObject | DistributionRange | RemoteDataSet | Array | Iterable}\n * inputData - A URI-shaped string, a [Multi]RangeObject-constructing value, or\n * an array of slice data\n * @return {RangeObject | RangeLike | DistributionRange | RemoteDataSet | Array}\n * The coerced input in an appropriate format ([Multi]RangeObject,\n * DistributionRange, RemoteDataSet, or array)\n */\n const wrangleData = (inputData) => {\n\n if (RangeObject.isRangelike(inputData)) { return inputData }\n if (RangeObject.isRangeObject(inputData)) { return inputData }\n if (DistributionRange.isDistribution(inputData)) { return inputData }\n if (inputData instanceof SparseRangeObject) { return inputData }\n if (inputData instanceof MultiRangeObject) { return inputData }\n if (MultiRangeObject.isProtoMultiRangelike(inputData)) { return new MultiRangeObject(inputData) }\n if (RangeObject.isProtoRangelike(inputData)) { return new RangeObject(inputData) }\n if (DistributionRange.isProtoDistribution(inputData)) { return new DistributionRange(inputData) }\n if (RemoteDataSet.isRemoteDataSet(inputData)) { return inputData }\n if (RemoteDataPattern.isRemoteDataPattern(inputData)) { return inputData }\n\n return Array.isArray(inputData) ? inputData : [inputData];\n};\n\n/**\n * @classdesc The Compute API's Job Handle (see {@link https://docs.dcp.dev/specs/compute-api.html#job-handles|Compute API spec})\n * Job handles are objects which correspond to jobs. \n * They are created by some exports of the compute module, such as {@link module:dcp/compute.do|compute.do} and {@link module:dcp/compute.for|compute.for}.\n * @extends module:dcp/dcp-events.PropagatingEventEmitter\n * @hideconstructor\n * @access public\n */\nclass Job extends PropagatingEventEmitter\n{\n /**\n * Fired when the job is accepted by the scheduler on deploy.\n * \n * @event Job#accepted\n * @access public\n * @type {object}\n *//**\n * Fired when the job is cancelled.\n * \n * @event Job#cancel\n * @access public\n *//**\n * Fired when a result is returned.\n * \n * @event Job#result\n * @access public\n * @type {object}\n * @property {string} jobAddress Address of the job\n * @property {string} task ID of the task (slice) the result came from\n * @property {number} sort The index of the slice\n * @property {object} result\n * @property {string} result.request\n * @property {*} result.result The value returned from the work function\n *//**\n * Fired when the result handle is modified, either when a new `result` event is fired or when the results are populated with `results.fetch()`\n * \n * @event Job#resultsUpdated\n * @access public\n *//**\n * Fired when the job has been completed.\n * \n * @event Job#complete\n * @access public\n * @type {ResultHandle}\n *//**\n * Fired when the job's status changes.\n * \n * @event Job#status\n * @access public\n * @type {object}\n * @property {string} jobAddress Address of the job\n * @property {number} total Total number of slices in the job\n * @property {number} distributed Number of slices that have been distributed\n * @property {number} computed Number of slices that have completed execution (returned a result)\n * @property {string} runStatus Current runStatus of the job\n *//**\n * Fired when a slice throws an error.\n * \n * @event Job#error\n * @access public\n * @type {object}\n * @property {string} jobAddress Address of the job\n * @property {number} sliceIndex Index of the slice that threw the error\n * @property {string} message The error message\n * @property {string} stack The error stacktrace\n * @property {string} name The error type name\n *//**\n * Fired when a slice uses one of the console log functions.\n * \n * @event Job#console\n * @access public\n * @type {object}\n * @property {string} jobAddress Address of the job\n * @property {number} sliceIndex The index of the slice that produced this event\n * @property {string} level The log level, one of `debug`, `info`, `log`, `warn`, or `error`\n * @property {string} message The console log message\n *//**\n * Fired when a slice is stopped for not calling progress. Contains information about how long the slice ran for, and about the last reported progress calls.\n * \n * @event Job#noProgress\n * @access public\n * @type {object}\n * @property {string} jobAddress Address of the job\n * @property {number} sliceIndex The index of the slice that failed due to no progress\n * @property {number} timestamp How long the slice ran before failing\n * @property {object} progressReports\n * @property {object} progressReports.last The last progress report received from the worker\n * @property {number} progressReports.last.timestamp Time since the start of the slice\n * @property {number} progressReports.last.progress Progress value reported\n * @property {*} progressReports.last.value The last value that was passed to the progress function\n * @property {number} progressReports.last.throttledReports Number of calls to progress that were throttled since the last report\n * @property {object} progressReports.lastUpdate The last determinate (update to the progress param) progress report received from the worker\n * @property {number} progressReports.lastUpdate.timestamp\n * @property {number} progressReports.lastUpdate.progress\n * @property {*} progressReports.lastUpdate.value\n * @property {number} progressReports.lastUpdate.throttledReports\n *//**\n @todo: is this in the spec? is there a no progress data? should there be?\n * Identical to `noProgress`, except that it also contains the data that the slice was executed with.\n * \n * @event Job#noProgressData\n * @access public\n * @type {object}\n * @property {*} data The data that the slice was executed with\n *//**\n * Fired when the job is paused due to running out of funds. The job can be resumed by escrowing more funds then resuming the job.\n * @todo: is this a thing, should it be a thing (the payload)\n * Event payload is the estimated funds required to complete the job\n * \n * @event Job#nofunds\n * @access public\n * @type {BigNumber}\n *//**\n * Fired when the job cannot be deployed due to no bank account / not enough balance to deploy the job\n * \n * @event Job#ENOFUNDS\n * @access public\n *//**\n * Fired when the job is cancelled due to the work function not calling the `progress` method frequently enough.\n * \n * @event Job#ENOPROGRESS\n * @access public\n *//**\n * The job was cancelled because scheduler has determined that individual tasks in this job exceed the maximum allowable execution time.\n * \n * @event Job#ESLICETOOSLOW\n * @access public\n *//**\n * Fired when the job is cancelled because too many work functions are terminating with uncaught exceptions.\n * \n * @event Job#ETOOMANYERRORS\n * @access public\n */\n\n /**\n * @form1 new Job('application_worker_address'[, data[, arguments]])\n * @form2a new Job('worker source'[, data[, arguments]])\n * @form2b new Job(worker_function[, data[, arguments]])\n */\n constructor ()\n {\n super('Job');\n if (typeof arguments[0] === 'function')\n arguments[0] = arguments[0].toString();\n\n if (typeof arguments[0] === 'string')\n {\n const { encodeDataURI } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\n this.workFunctionURI = encodeDataURI(arguments[0], 'application/javascript');\n } \n else if (DcpURL.isURL(arguments[0]))\n this.workFunctionURI = arguments[0].href;\n\n this.jobInputData = wrangleData(arguments[1] || []);\n this.jobArguments = wrangleData(arguments[2] || []);\n \n log('num wrangledInputData:', this.jobInputData.length);\n log('num wrangledArguments:', this.jobArguments.length);\n\n this.initEventSystems();\n\n /**\n * An object describing the cost the user believes each the average slice will incur, in terms of CPU/GPU and I/O.\n * If defined, this object is used to provide initial scheduling hints and to calculate escrow amounts.\n *\n * @type {object}\n * @access public\n */\n this.initialSliceProfile = undefined;\n \n // The max value that the client is willing to spend to deploy\n // (list on the scheduler, doesn't include compute payment)\n // maxDeployPayment is the max the user is willing to pay to DCP (as a\n // Hold), in addition to the per-slice offer and associated scrape.\n // Currently calculated as `deployCost = costPerKB *\n // (JSON.stringify(job).length / 1024) // 1e-9 per kb`\n // @todo: figure this out / er nov 2018\n this.maxDeployPayment = 1;\n\n /**\n * An object describing the requirements that workers must have to be eligible for this job. The default values are set in the job-submitter, and only the client specified\n * requirements are sent over the wire. See {@link https://docs.dcp.dev/specs/compute-api.html#requirements-objects|Requirements Objects}.\n *\n * @type {object}\n * @access public\n */\n this.requirements = {};\n\n /**\n * A place to store public-facing attributes of the job. Anything stored on this object will be available inside the work \n * function (see {@link module:dcp/compute~sandboxEnv.work}). The properties documented here may be used by workers to display what jobs are currently being \n * worked on.\n * @access public\n * @property {string} name Public-facing name of this job.\n * @property {string} description Public-facing description for this job.\n * @property {string} link Public-facing link to external resource about this job.\n */\n this.public = {\n name: null,\n description: null,\n link: null,\n };\n \n /**\n * A cryptographic receipt indicating deployment of the job on the scheduler\n * @type {object}\n * @access public\n */\n this.receipt = null;\n \n /**\n * a SliceProfile object which contains the average costs for the slices which have been computed to date.\n * Until the first result is returned, this property is undefined.\n * @type {object}\n * @access public\n */\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 * When true, allows a job in estimation to have requestTask return multiple estimation slices.\n * This flag applies independent of infinite estimation, viz., this.estimationSlices === null .\n * @type {boolean}\n * @access public\n */\n this.greedyEstimation = false;\n /**\n * tunable parameters per job\n * @access public\n * @param {object} tuning \n * @param {string} tuning.kvin Encode the TypedArray into a string, trying multiple methods to determine optimum \n * size/performance. The this.tune variable affects the behavior of this code this:\n * @param {boolean} speed If true, only do naive encoding: floats get represented as byte-per-digit strings\n * @param {boolean} size If true, try the naive, ab8, and ab16 encodings; pick the smallest\n * If both are false try the naive encoding if under typedArrayPackThreshold and use if smaller\n * than ab8; otherwise, use ab8\n */\n this.tuning = {\n kvin: {\n size: false,\n speed: false,\n },\n }\n /* For API interface to end-users only */\n Object.defineProperty(this, 'id', {\n get: () => this.address,\n set: (id) => { this.address = id }\n });\n \n this.uuid = uuidv4(); /** @see {@link https://kingsds.atlassian.net/browse/DCP-1475?atlOrigin=eyJpIjoiNzg3NmEzOWE0OWI4NGZkNmI5NjU0MWNmZGY2OTYzZDUiLCJwIjoiaiJ9|Jira Issue} */\n this.dependencies = []; /* dependencies of the work function */\n this.requirePath = []; /* require path for dependencies */\n this.modulePath = []; /* path to module that invoked .exec() for job */\n this.connected = false; /* true when exec or resume called */\n this.results = new ResultHandle(this); /* result handle */\n this.collateResults = true; /* option to receive results as they are computed & ensure all are received on finish */\n this.contextId = null; /* optional string which is used to indicate to caching mechanisms different keystores with same name */ \n this.force100pctCPUDensity = false; /* tell scheduler to assume this job uses 100% cpu density */\n this.workerConsole = false; /* tell workers to log more information about failures in the evaluator this job causes */\n this.address = null; /* job address, created by scheduler during exec call. */\n this.paymentAccountKeystore = null; /* keystore for payment for job to come from */\n this.status = { /* job status details */\n runStatus: null,\n total: null,\n distributed: null,\n computed: null\n };\n \n // service locations\n this.scheduler = dcpConfig.scheduler.services.jobSubmit.location;\n this.bank = dcpConfig.bank.services.bankTeller.location;\n \n // Compute groups. Add to public compute group by default\n this.computeGroups = [ Object.assign({}, schedulerConstants.computeGroups.public) ];\n \n // Update the ready state as we go through job deployment\n this.readyState = sliceStatus.new;\n const that = this;\n this.readyStateChange = function job$$readyStateChange (readyState)\n {\n that.readyState = readyState;\n that.emit('readyStateChange', that.readyState);\n }\n }\n \n /**\n * Initialize the various event systems the job handle requires. These include:\n * - an internal event emitter (this.ee)\n * - an event emitter for any events emitted on `work.emit` within work functions (this.work)\n * - an event subscriber to subscribe (to receive) events from the scheduler (this.eventSubscriber)\n */\n initEventSystems ()\n {\n // Handle the various event-related things required in the constructor\n\n // Internal event emitter for events within job handle\n this.ee = new EventEmitter('Job Internal');\n\n /**\n * An EventEmitter for custom events dispatched by the work function.\n * @type {module:dcp/dcp-events.EventEmitter}\n * @access public\n * @example\n * // in work function\n * work.emit('myEventName', 1, [2], \"three\");\n * // client-side\n * job.work.on('myEventName', (num, arr, string) => { });\n */\n this.work = new EventEmitter('job.work');\n this.listenForCustomEvents = false;\n\n // Initialize the eventSubscriber so each job has unique eventSubscriber\n this.eventSubscriber = new ((__webpack_require__(/*! dcp/events/event-subscriber */ \"./src/events/event-subscriber.js\").EventSubscriber))(this);\n\n // Some events from the event subscriber can't be emitted immediately upon receipt without having \n // weird/wrong output due to things like serialization. We allow interceptors in the event subscriber\n // to handle this.\n const that = this\n var lastConsoleEv;\n var sameCounter = 1;\n const parseConsole = function deserializeConsoleMessage(ev) {\n if (tunedKvin)\n ev.message = tunedKvin.unmarshal(ev.message);\n else \n ev.message = kvin.unmarshal(ev.message);\n \n if (lastConsoleEv && ev.message[0] === lastConsoleEv.message[0] && ev.sliceNumber === lastConsoleEv.sliceNumber && ev.level === lastConsoleEv.level)\n ev.same = ++sameCounter;\n else\n sameCounter = 1;\n lastConsoleEv = ev;\n \n /* if we have the same message being logged (same sliceNumber, message, log level), the console event object will have the sole property same, nothing else */\n if (ev.same > 1)\n that.emit('console', { same: ev.same });\n else\n {\n delete ev.same;\n that.emit('console', ev);\n }\n }\n\n this.eventIntercepts = {\n result: (ev) => this.handleResult(ev),\n status: (ev) => this.handleStatus(ev),\n cancel: (ev) => this.ee.emit('stopped', ev),\n custom: (ev) => this.work.emit(ev.customEvent, ev),\n console: parseConsole,\n };\n \n this.eventTypes = (__webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\").eventTypes);\n\n this.work.on('newListener', (evt) => {\n this.listenForCustomEvents = true;\n });\n this.desiredEvents = []\n this.on('newListener', (evt) => {\n if (!this.connected && evt !== 'newListener')\n this.desiredEvents.push(evt);\n if (evt === 'cancel')\n this.listeningForCancel = true;\n });\n }\n \n /** \n * Cancel the job\n * @access public\n * @param {string} reason If provided, will be sent to client\n */\n async cancel (reason = undefined)\n {\n const response = await this.useDeployConnection('cancelJob', {\n job: this.address,\n owner: this.paymentAccountKeystore.address,\n reason,\n }, this.paymentAccountKeystore);\n\n return response.payload;\n }\n\n /** \n * Resume this job\n * @access public\n */\n async resume ()\n {\n const response = await this.schedulerConnection.send('resumeJob', {\n job: this.address,\n owner: this.paymentAccountKeystore.address,\n }, this.paymentAccountKeystore);\n\n return response.payload;\n }\n\n /**\n * Helper function for retrieving info about the job. The job must have already been deployed.\n * An alias for {@link module:dcp/compute.getJobInfo}.\n * @access public\n */\n getJobInfo ()\n {\n return (__webpack_require__(/*! ../compute */ \"./src/dcp-client/compute.js\").compute.getJobInfo)(this.address);\n }\n \n /**\n * Helper function for retrieving info about the job's slices. The job must have already been deployed.\n * An alias for {@link module:dcp/compute.getSliceInfo}.\n * @access public\n */\n getSliceInfo ()\n {\n return (__webpack_require__(/*! ../compute */ \"./src/dcp-client/compute.js\").compute.getSliceInfo)(this.address);\n }\n \n /** Escrow additional funds for this job\n * @access public\n * @param {number|BigNumber} fundsRequired - A number or BigNumber instance representing the funds to escrow for this job\n */\n async escrow (fundsRequired)\n {\n if ((typeof fundsRequired !== 'number' && !BigNumber.isBigNumber(fundsRequired))\n || fundsRequired <= 0 || !Number.isFinite(fundsRequired) || Number.isNaN(fundsRequired))\n throw new Error(`Job.escrow: fundsRequired must be a number greater than zero. (not ${fundsRequired})`);\n\n const bankConnection = new protocolV4.Connection(dcpConfig.bank.services.bankTeller);\n\n /*\n * escrow has been broken for an unknown amount of time. `feeStructureId` is not defined anywhere in the job class, and hasn't\n * for a period of time. When fixed, `this[INTERNAL_SYMBOL].payloadDetails.feeStructureId` will likely become just `this.feeStructureId`,\n * but it's being left alone until someone spends the time to fix escrow. / rs Jul 2022\n */\n const response = await bankConnection.send('embiggenFeeStructure', {\n feeStructureAddress: this[INTERNAL_SYMBOL].payloadDetails.feeStructureId,\n additionalEscrow: new BigNumber(fundsRequired),\n fromAddress: this.paymentAccountKeystore.address,\n }, this.paymentAccountKeystore);\n\n bankConnection.close();\n const escrowReceipt = response.payload;\n return escrowReceipt;\n }\n\n /**\n * create bundles for local dependencies\n */\n _pack ()\n {\n var retval = (__webpack_require__(/*! ./node-modules */ \"./src/dcp-client/job/node-modules.js\").createModuleBundle)(this.dependencies);\n return retval;\n }\n\n /** \n * Collect all of the dependencies together, throw them into a BravoJS\n * module which sideloads them as a side effect of declaration, and transmit\n * them to the package manager. Then we return the package descriptor object,\n * which is guaranteed to have only one file in it.\n *\n * @returns {object} with properties name and files[0]\n */\n async _publishLocalModules()\n {\n const dcpPublish = __webpack_require__(/*! dcp/common/dcp-publish */ \"./src/common/dcp-publish.js\");\n \n const { tempFile, hash, unresolved } = await this._pack();\n\n if (!tempFile) {\n return { unresolved };\n }\n\n const sideloaderFilename = tempFile.filename;\n const pkg = {\n name: `dcp-pkg-v1-localhost-${hash.toString('hex')}`,\n version: '1.0.0',\n files: {\n [sideloaderFilename]: `${sideloaderModuleIdentifier}.js`,\n },\n }\n\n await dcpPublish.publish(pkg);\n tempFile.remove();\n\n return { pkg, unresolved };\n }\n \n /**\n * This function specifies a module dependency (when the argument is a string)\n * or a list of dependencies (when the argument is an array) of the work\n * function. This function can be invoked multiple times before deployment.\n * @param {string | string[]} modulePaths - A string or array describing one\n * or more dependencies of the job.\n * @access public\n */\n requires (modulePaths)\n {\n if (typeof modulePaths !== 'string' && (!Array.isArray(modulePaths) || modulePaths.some((modulePath) => typeof modulePath !== 'string')))\n throw new TypeError('The argument to dependencies is not a string or an array of strings');\n else if (modulePaths.length === 0)\n throw new RangeError('The argument to dependencies cannot be an empty string or array');\n else if (Array.isArray(modulePaths) && modulePaths.some((modulePath) => modulePath.length === 0))\n throw new RangeError('The argument to dependencies cannot be an array containing an empty string');\n\n if (!Array.isArray(modulePaths))\n modulePaths = [modulePaths];\n\n for (const modulePath of modulePaths)\n {\n if (modulePath[0] !== '.' && modulePath.indexOf('/') !== -1)\n {\n const modulePrefixRegEx = /^(.*)\\/.*?$/;\n const [, modulePrefix] = modulePath.match(modulePrefixRegEx);\n if (modulePrefix && this.requirePath.indexOf(modulePrefix) === -1)\n this.requirePath.push(modulePrefix);\n }\n this.dependencies.push(modulePath);\n }\n }\n \n /** Set the account upon which funds will be drawn to pay for the job.\n * @param {module:dcp/wallet.AuthKeystore} keystore A keystore that representa a bank account.\n * @access public\n */\n setPaymentAccountKeystore (keystore)\n {\n if (this.address)\n {\n if (!keystore.address.eq(this.paymentAccountKeystore))\n {\n let message = 'Cannot change payment account after job has been deployed';\n this.emit('EPERM', message);\n throw new Error(`EPERM: ${message}`);\n }\n }\n \n if (!(keystore instanceof wallet.Keystore))\n throw new Error('Not an instance of Keystore: ' + keystore.toString());\n this.paymentAccountKeystore = keystore;\n }\n \n /** Set the slice payment offer. This is equivalent to the first argument to exec.\n * @param {number} slicePaymentOffer - The number of DCC the user is willing to pay to compute one slice of this job\n */\n setSlicePaymentOffer (slicePaymentOffer)\n {\n this.slicePaymentOffer = new SlicePaymentOffer(slicePaymentOffer);\n }\n \n \n /**\n * @param {URL|DcpURL} locationUrl - A URL object\n * @param {object} postParams - An object with any parameters that a user would like to be passed to a \n * remote result location. This object is capable of carry API keys for S3, \n * DropBox, etc. These parameters are passed as parameters in an \n * application/x-www-form-urlencoded request.\n */\n setResultStorage (locationUrl, postParams)\n {\n if (locationUrl instanceof URL || locationUrl instanceof DcpURL)\n this.resultStorageDetails = locationUrl;\n else\n throw new Error('Not an instance of a DCP URL: ' + locationUrl);\n \n\n // resultStorageParams contains any post params required for off-prem storage\n if (typeof postParams !== 'undefined' && typeof postParams === 'object' )\n this.resultStorageParams = postParams;\n else\n throw new Error('Not an instance of a object: ' + postParams);\n\n // Some type of object here\n this.resultStorageType = 'pattern';\n }\n \n /**\n * This function is identical to exec, except that the job is executed locally\n * in the client.\n * @async\n * @param {number} cores - the number of local cores in which to execute the job.\n * @param {...any} args - The remaining arguments are identical to the arguments of exec\n * @return {Promise<ResultHandle>} - resolves with the results of the job, rejects on an error\n * @access public\n */\n async localExec (cores = 1, ...args)\n {\n this.inLocalExec = true;\n this.estimationSlices = 0;\n this.greedyEstimation = false;\n this.isCI = false;\n\n let worker;\n this.on('accepted', () => {\n // Start a worker for this job\n worker = new Worker({\n localExec: true,\n jobAddresses: [this.address],\n allowedOrigins: this.localExecAllowedFiles,\n paymentAddress: this.paymentAccountKeystore.address,\n identity: this.identityKeystore,\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.on('error', (error) => {\n console.error('Worker Error:', error);\n });\n \n worker.on('warning', (warning) => {\n console.warn('Worker Warning:', warning);\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 if (DCP_ENV.platform === 'nodejs')\n {\n this.localExecAllowedFiles =\n {\n any: [],\n fetchData: [],\n fetchWorkFunctions: [],\n fetchArguments: [],\n sendResults: [],\n };\n \n // Determine type of input data\n const { dataRange, dataValues, dataPattern, sliceCount } = marshalInputData(this.jobInputData);\n \n const inputSetFiles = [];\n \n let inputSetURIs = [];\n let dataSet;\n \n if (dataValues)\n {\n for (let i = 0; i < dataValues.length; i++)\n {\n if (!(dataValues[i] instanceof URL))\n {\n let marshaledInputValue = kvinMarshal(dataValues[i]);\n let inputDataFile = createTempFile('dcp-localExec-sliceData-XXXXXXXXX', 'kvin');\n inputDataFile.writeSync(JSON.stringify(marshaledInputValue));\n inputSetFiles.push(inputDataFile);\n inputSetURIs.push(new URL('file://' + inputDataFile.filename));\n }\n else\n {\n inputSetURIs.push(dataValues[i]);\n this.localExecAllowedFiles['fetchData'].push(dataValues[i].origin);\n }\n }\n dataSet = new RemoteDataSet(inputSetURIs);\n if (dataSet.length > 0)\n this.marshaledDataValues = dataSet;\n }\n if (dataRange)\n {\n inputSetFiles.push(createTempFile('dcp-localExec-sliceData-XXXXXXXXX', 'json'));\n let marshaledInputSet = JSON.stringify(dataRange);\n inputSetFiles[0].writeSync(marshaledInputSet)\n inputSetURIs.push(new URL('file://' + inputSetFiles[0].filename));\n dataSet = new RemoteDataSet(inputSetURIs);\n this.marshaledDataRange = dataSet;\n this.rangeLength = dataRange.length;\n }\n \n if (dataPattern)\n {\n let uri = dataPattern;\n for (let i = 0; i < sliceCount; i++)\n {\n let sliceNum = i+1;\n let newURI = new URL(uri.replace('{slice}', sliceNum.toString()));\n this.localExecAllowedFiles['fetchData'].push(newURI.origin);\n }\n }\n \n // For allowed origins of the localexec worker. Only allow the origins (files in this case) in this list.\n for (let i = 0; i < inputSetFiles.length; i++)\n this.localExecAllowedFiles['fetchData'].push(inputSetFiles[i].filename);\n \n // Save work function to disk if work function starts with data (ie not remote)\n if (this.workFunctionURI.startsWith('data:'))\n {\n const workFunctionFile = createTempFile('dcp-localExec-workFunction-XXXXXXXXX', 'js');\n const workFunction = await fetchURI(this.workFunctionURI);\n workFunctionFile.writeSync(workFunction);\n \n const workFunctionFileURL = new URL('file://' + workFunctionFile);\n this.workFunctionURI = workFunctionFileURL.href;\n this.localExecAllowedFiles['fetchWorkFunctions'].push(workFunctionFile.filename);\n }\n else\n this.localExecAllowedFiles['fetchWorkFunctions'].push(new URL(this.workFunctionURI).origin);\n \n let encodedJobArgumentUris = [];\n if (this.jobArguments)\n {\n if (this.jobArguments instanceof RemoteDataPattern) /* Not supported */\n throw new DCPError('Cannot use RemoteDataPattern as work function arguments', 'EBADARG')\n if (this.jobArguments instanceof RemoteDataSet) /* Entire set is RemoteDataSet */\n {\n this.jobArguments.forEach((e) =>\n {\n this.localExecAllowedFiles['fetchArguments'].push(new URL(e).origin);\n encodedJobArgumentUris.push(encodeJobValueUri(new URL(e)));\n });\n }\n else\n {\n for (let i = 0; i < this.jobArguments.length; i++)\n {\n if (this.jobArguments[i] instanceof URL)\n {\n this.localExecAllowedFiles['fetchArguments'].push(this.jobArguments[i].origin);\n encodedJobArgumentUris.push(encodeJobValueUri(this.jobArguments[i]));\n }\n else\n {\n if (this.jobArguments[i] instanceof RemoteDataSet) /* Member of set is RemoteDataSet */\n {\n this.jobArguments[i].forEach((e) =>\n {\n this.localExecAllowedFiles['fetchArguments'].push(new URL(e).origin);\n encodedJobArgumentUris.push(encodeJobValueUri(new URL(e)));\n });\n }\n else /* Actual Value */\n {\n const localArgFile = createTempFile(`dcp-localExec-argument-${i}-XXXXXXXXX`, 'kvin');\n localArgFile.writeSync(JSON.stringify(kvinMarshal(this.jobArguments[i])));\n this.localExecAllowedFiles['fetchArguments'].push(localArgFile.filename);\n encodedJobArgumentUris.push(encodeJobValueUri(new URL('file://' + localArgFile.filename)));\n }\n }\n } \n }\n }\n this.marshaledArguments = kvinMarshal(encodedJobArgumentUris);\n }\n \n return this.exec(...args).finally(() => {\n if (worker) {\n setTimeout(() => {\n // stop the worker\n worker.stop(true);\n }, 3000);\n }\n });\n }\n\n /**\n * Deploys the job to the scheduler.\n * @param {number | object} [slicePaymentOffer=compute.marketValue] - Amount\n * in DCC that the user is willing to pay per slice.\n * @param {Keystore} [paymentAccountKeystore=wallet.get] - An instance of the\n * Wallet API Keystore that's used as the payment account when executing the\n * job.\n * @param {object} [initialSliceProfile] - An object describing the cost the\n * user believes the average slice will incur.\n * @access public\n * @emits Job#accepted\n */\n async exec (slicePaymentOffer = (__webpack_require__(/*! ../compute */ \"./src/dcp-client/compute.js\").compute.marketValue), paymentAccountKeystore, initialSliceProfile)\n {\n if (this.connected)\n throw new Error('Exec called twice on the same job handle.');\n \n if (this.estimationSlices === Infinity)\n this.estimationSlices = null;\n else if (this.estimationSlices < 0)\n throw new Error('Incorrect value for estimationSlices; it can be an integer or Infinity!');\n \n if (this.tuning.kvin.speed || this.tuning.kvin.size)\n {\n tunedKvin = new kvin.KVIN();\n tunedKvin.tune = 'size';\n if(this.tuning.kvin.speed)\n tunedKvin.tune = 'speed';\n // If both size and speed are true, kvin will optimize based on speed\n if(this.tuning.kvin.speed && this.tuning.kvin.size)\n console.log('Slices and arguments are being uploaded with speed optimization.');\n }\n \n /* slight optimization to ensure we don't send requirements that will be ignored in the job submitter. Make a copy of the client specified requirements for this so that we dont magically override something they manually set */\n const _DEFAULT_REQUIREMENTS = JSON.parse(JSON.stringify(DEFAULT_REQUIREMENTS));\n removeBadRequirements(this.requirements, _DEFAULT_REQUIREMENTS);\n \n this.readyStateChange('exec');\n if ((typeof slicePaymentOffer === 'number') || (typeof slicePaymentOffer === 'object')\n || ((this.slicePaymentOffer === null || this.slicePaymentOffer === undefined) && typeof slicePaymentOffer === 'function'))\n this.setSlicePaymentOffer(slicePaymentOffer);\n if (typeof initialSliceProfile !== 'undefined')\n this.initialSliceProfile = initialSliceProfile;\n \n if (typeof paymentAccountKeystore !== 'undefined')\n {\n /** XXX @todo deprecate use of ethereum wallet objects */\n if (typeof paymentAccountKeystore === 'object' && paymentAccountKeystore.hasOwnProperty('_privKey'))\n {\n console.warn('* deprecated API * - job.exec invoked with ethereum wallet object as paymentAccountKeystore') /* /wg oct 2019 */\n paymentAccountKeystore = paymentAccountKeystore._privKey\n }\n /** XXX @todo deprecate use of private keys */\n if (wallet.isPrivateKey(paymentAccountKeystore))\n {\n console.warn('* deprecated API * - job.exec invoked with private key as paymentAccountKeystore') /* /wg dec 2019 */\n paymentAccountKeystore = await new wallet.Keystore(paymentAccountKeystore, '');\n }\n\n this.setPaymentAccountKeystore(paymentAccountKeystore)\n }\n \n if (this.paymentAccountKeystore)\n // Throws if they fail to unlock, we allow this since the keystore was set programmatically. \n await this.paymentAccountKeystore.unlock(undefined, parseFloat(dcpConfig.job.maxDeployTime));\n else\n {\n // If not set programmatically, we keep trying to get an unlocked keystore ... forever.\n let locked = true;\n let safety = 0; // no while loop shall go unguarded\n let ks;\n do\n {\n ks = null;\n // custom message for the browser modal to denote the purpose of keystore submission\n let msg = `This application is requesting a keystore file to execute ${this.public.description || this.public.name || 'this job'}. Please upload the corresponding keystore file. If you upload a keystore file which has been encrypted with a passphrase, the application will not be able to use it until it prompts for a passphrase and you enter it.`;\n try\n {\n ks = await wallet.get({ contextId: this.contextId, jobName: this.public.name, msg});\n }\n catch (e)\n {\n if (e.code !== ClientModal.CancelErrorCode) throw e;\n };\n if (ks)\n {\n try\n {\n await ks.unlock(undefined, parseFloat(dcpConfig.job.maxDeployTime));\n locked = false;\n }\n catch (e)\n {\n // prompt user again if user enters password incorrectly, exit modal otherwise\n if (e.code !== wallet.unlockFailErrorCode) throw e;\n }\n }\n if (safety++ > 1000) throw new Error('EINFINITY: job.exec tried wallet.get more than 1000 times.')\n } while (locked);\n this.setPaymentAccountKeystore(ks)\n }\n \n // We either have a valid keystore + password or we have rejected by this point.\n if (!this.slicePaymentOffer)\n throw new Error('A payment profile must be assigned before executing the job');\n else\n this.feeStructure = this.slicePaymentOffer.toFeeStructure(this.jobInputData.length);\n\n if (!this.address)\n {\n try\n {\n this.readyStateChange('init');\n await this.deployJob();\n const listenersPromise = this.addInitialEvents();\n const computeGroupsPromise = this.joinComputeGroups();\n let uploadSlicePromise;\n // if job data is by value then upload data to the scheduler in a staggered fashion\n if (Array.isArray(this.dataValues) && !this.marshaledDataValues)\n {\n this.readyStateChange('uploading');\n uploadSlicePromise = addSlices(this.dataValues, this.address, tunedKvin)\n .then(() => {\n debugging('slice-upload') && console.info(`970: slice data uploaded, closing job...`);\n return this.close();\n });\n }\n \n // await all promises for operations that can be done after the job is deployed\n await Promise.all([listenersPromise, computeGroupsPromise, uploadSlicePromise]);\n \n this.readyStateChange('deployed');\n this.emit('accepted', { job: this });\n }\n catch (error)\n {\n if (ON_BROWSER)\n await ClientModal.alert(error, { title: 'Failed to deploy job!' });\n throw error;\n }\n }\n else\n {\n // reconnecting to an old job\n await this.addInitialEvents();\n this.readyStateChange('reconnected');\n }\n\n this.connected = true;\n\n return new Promise((resolve, reject) => {\n const onComplete = () => resolve(this.results);\n const onCancel = (event) => {\n /**\n * FIXME(DCP-1150): Remove this since normal cancel event is noisy\n * enough to not need stopped event too.\n */\n if (ON_BROWSER && !this.listeningForCancel)\n ClientModal.alert('More details in console...', { title: 'Job Canceled' });\n this.emit('cancel', event);\n\n let errorMsg = event.reason;\n if (event.error && event.error !== 'undefined')\n errorMsg = errorMsg +`\\n Recent error message: ${event.error.message}`\n \n reject(new DCPError(errorMsg, event.code));\n };\n\n this.ee.once('stopped', async (stopEvent) => {\n // There is a chance the result submitter will emit finished > 1 time. Only handle it once.\n if (this.receivedStop)\n return;\n this.receivedStop = true;\n this.emit('stopped', stopEvent.runStatus);\n switch (stopEvent.runStatus) {\n case jobStatus.finished:\n if (this.collateResults)\n {\n let report = await this.getJobInfo();\n let allSliceNumbers = Array.from(Array(report.totalSlices)).map((e,i)=>i+1);\n let remainSliceNumbers = allSliceNumbers.filter((e) => !this.results.isAvailable(e));\n\n if (remainSliceNumbers.length)\n {\n const promises = remainSliceNumbers.map(sliceNumber => this.results.fetch([sliceNumber], true));\n await Promise.all(promises);\n }\n }\n\n this.emit('complete', this.results);\n onComplete();\n break;\n case jobStatus.cancelled:\n onCancel(stopEvent);\n break;\n default:\n /**\n * Asserting that we should never be able to reach here. The only\n * scheduler events that should trigger the Job's 'stopped' event\n * are jobStatus.cancelled, jobStatus.finished, and sliceStatus.paused.\n */\n reject(new Error(`Unknown event \"${stopEvent.runStatus}\" caused the job to be stopped.`));\n break;\n }\n });\n\n }).finally(() => {\n const handleErr = (e) => {\n console.error('Error while closing job connection:');\n console.error(e);\n }\n\n // Create an async IIFE to not block the promise chain\n (async () => {\n // delay to let last few events to be received\n await new Promise((resolve) => setTimeout(resolve, 1000));\n \n // close all of the connections so that we don't cause node processes to hang.\n this.closeDeployConnection();\n await this.eventSubscriber.close().catch(handleErr);\n await computeGroups.closeServiceConnection().catch((err) => {\n console.error('Warning: could not close compute groups service connection', err);\n });\n })();\n });\n }\n \n /**\n * job.addListeners(): Private function used to set up event listeners to the scheduler\n * before deploying the job.\n */\n async addInitialEvents ()\n {\n this.readyStateChange('listeners');\n\n // This is important: We need to flush the task queue before adding listeners\n // because we queue pending listeners by listening to the newListener event (in the constructor).\n // If we don't flush here, then the newListener events may fire after this function has run,\n // and the events won't be properly set up.\n await new Promise(resolve => setTimeout(resolve, 0));\n\n // @todo: Listen for an estimated cost, probably emit an \"estimated\" event when it comes in?\n // also @todo: Do the estimation task(s) on the scheduler and send an \"estimated\" event\n\n // Always listen to the stop event. It will resolve the work function promise, so is always needed.\n this.on('stop', (ev) => {this.ee.emit('stopped', ev)});\n\n // Connect listeners that were set up before exec\n if (this.desiredEvents.includes('result'))\n this.listeningForResults = true;\n await this.subscribeNewEvents(this.desiredEvents);\n\n // Connect listeners that are set up after exec\n this.on('newListener', (evt) => {\n if (evt === 'newListener' || this.desiredEvents.includes(evt))\n return;\n this.subscribeNewEvents([evt]);\n });\n \n // automatically add a listener for results if collateResults is on\n if (this.collateResults && !this.listeningForResults)\n this.on('result', () => {});\n\n debugging('dcp-client') && console.debug('subscribedEvents', this.desiredEvents);\n\n // If we have listeners for job.work, subscribe to custom events\n if (this.listenForCustomEvents)\n await this.subscribeCustomEvents();\n // Connect work event listeners that are set up after exec\n else\n this.work.on('newListener', () => this.subscribeCustomEvents());\n }\n \n /**\n * Subscribes to either reliable events or optional events. It is assumed that\n * any call to this function will include only new events.\n * @param {string[]} events \n */\n async subscribeNewEvents (events)\n {\n const reliableEvents = [];\n const optionalEvents = [];\n for (let eventName of events)\n {\n eventName = eventName.toLowerCase();\n if (this.eventTypes[eventName] && this.eventTypes[eventName].reliable)\n reliableEvents.push(eventName);\n else if (this.eventTypes[eventName] && !this.eventTypes[eventName].reliable)\n optionalEvents.push(eventName);\n else\n debugging('dcp-client') && console.debug(`Job handler has listener ${eventName} which isn't an event-router event.`);\n }\n if (debugging('dcp-client'))\n {\n console.debug('reliableEvents', reliableEvents);\n console.debug('optionalEvents', optionalEvents);\n }\n await this.eventSubscriber.subscribeManyEvents(reliableEvents, optionalEvents, { filter: { job: this.address } });\n }\n \n /**\n * Establishes listeners for worker events when requested by the client\n */\n async subscribeCustomEvents ()\n {\n if (!this.listeningForCustomEvents)\n await this.eventSubscriber.subscribeManyEvents([], ['custom'], { filter: { job: this.address } });\n this.listeningForCustomEvents = true\n }\n \n async joinComputeGroups ()\n {\n // localExec jobs are not entered in any compute group.\n if (!this.inLocalExec && this.computeGroups)\n {\n this.readyStateChange('compute-groups');\n computeGroups.addRef(); // Just in case we're doing a Promise.all on multiple execs.\n\n // Add this job to its currently-defined compute groups (as well as public group, if included)\n let success;\n \n if (!Array.isArray(this.computeGroups)) \n throw new DCPError('Compute groups must be wrapped in an Array', 'DCPL-1101');\n\n for (let i = 0; i < this.computeGroups.length; i++)\n {\n let value = this.computeGroups[i];\n \n if (typeof value !== 'object')\n throw new DCPError(`This compute group: ${value[i]} must be an object`, 'DCPL-1102');\n \n if (value.joinKey && typeof value.joinKey !== 'string' && !(value.joinKey instanceof String))\n throw new DCPError(`This join key: ${value.joinKey} must be a string or a string literal`, 'DCPL-1103');\n else if (value.joinKeystore && !(value.joinKeystore instanceof wallet.Keystore))\n throw new DCPError(`This join Keystore: ${value.joinKeystore} must be an instance of wallet.Keystore`, 'DCPL-1104');\n else if (!value.joinKey && !value.joinKeystore)\n throw new DCPError('Compute group must contain a joinKey or a joinKeystore', 'DCPL-1105');\n }\n \n try\n {\n const cgPayload = await computeGroups.addJobToGroups(this.address, this.computeGroups);\n success = true; // To support older version of CG service where addJobToGroups had void/undefined return.\n if (cgPayload) success = cgPayload.success;\n debugging('dcp-client') && console.debug('job/index: addJobToGroups cgPayload:', cgPayload ? cgPayload : 'cgPayload is not defined; probably from legacy CG service.');\n }\n catch (e)\n {\n debugging('dcp-client') && console.debug('job/index: addJobToGroups threw exception:', e);\n success = false;\n }\n\n computeGroups.closeServiceConnection().catch((err) => {\n console.error('Warning: could not close compute groups service connection', err)\n });\n\n /* Could not put the job in any compute group, even though the user wanted it to run. Cancel the job. */\n if (!success)\n {\n await this.cancel('compute-groups::Unable to join any compute groups');\n throw new DCPError(`Access Denied::Failed to add job ${this.address} to any of the desired compute groups`, 'DCPL-1100');\n }\n }\n }\n \n /**\n * Takes result events as input, stores the result and fires off\n * events on the job handle as required. (result, duplicate-result)\n *\n * @param {object} ev - the event recieved from protocol.listen('/results/0xThisGenAdr')\n */\n async handleResult (ev)\n {\n if (this.results === null)\n // This should never happen - the onResult event should only be established/called\n // in addListeners which should also initialize the internal results array\n throw new Error('Job.onResult was invoked before initializing internal results');\n \n const { result: _result, time } = ev.result;\n debugging('dcp-client') && console.debug('handleResult', _result);\n let result = await fetchURI(_result);\n\n if (this.results.isAvailable(ev.sliceNumber))\n {\n const changed = JSON.stringify(this.results[ev.sliceNumber]) !== JSON.stringify(result);\n this.emit('duplicate-result', { sliceNumber: ev.sliceNumber, changed });\n }\n\n this.results.newResult(result, ev.sliceNumber);\n }\n \n /**\n * Receives status events from the scheduler, updates the local status object\n * and emits a 'status' event\n *\n * @param {object} ev - the status event received from\n * protocol.listen('/status/0xThisGenAdr')\n * @param {boolean} emitStatus - value indicating whether or not the status\n * event should be emitted\n */\n handleStatus ({ runStatus, total, distributed, computed }, emitStatus = true)\n {\n Object.assign(this.status, {\n runStatus,\n total,\n distributed,\n computed,\n });\n\n if (emitStatus)\n this.emit('status', { ...this.status, job: this.address });\n }\n \n /**\n * Sends a request to the scheduler to deploy the job.\n */\n async deployJob ()\n {\n var moduleDependencies; \n \n /* Send sideloader bundle to the package server */\n if (DCP_ENV.platform === 'nodejs' && this.dependencies.length)\n {\n try\n {\n let { pkg, unresolved } = await this._publishLocalModules();\n\n moduleDependencies = unresolved;\n if (pkg)\n moduleDependencies.push(pkg.name + '/' + sideloaderModuleIdentifier); \n }\n catch(error)\n {\n throw new DCPError(`Error trying to communicate with package manager server: ${error}`);\n }\n }\n else\n moduleDependencies = this.dependencies;\n \n this.readyStateChange('preauth');\n\n const adhocId = this.uuid.slice(this.uuid.length - 6, this.uuid.length);\n const schedId = await dcpConfig.scheduler.identity;\n // The following check is needed for when using dcp-rtlink and loading the config through source, instead of using the dcp-client bundle\n let schedIdAddress = schedId;\n if(schedId.address)\n schedIdAddress = schedId.address;\n this.identityKeystore = await wallet.getId();\n const preauthToken = await bankUtil.preAuthorizePayment(schedIdAddress, this.maxDeployPayment, this.paymentAccountKeystore);\n const { dataRange, dataValues, dataPattern, sliceCount } = marshalInputData(this.jobInputData);\n if(dataValues)\n this.dataValues = dataValues;\n\n this.readyStateChange('deploying');\n\n /* Payload format is documented in scheduler-v4/libexec/job-submit/operations/submit.js */\n const submitPayload = {\n owner: this.identityKeystore.address,\n paymentAccount: this.paymentAccountKeystore.address,\n priority: 0, // @nyi\n\n workFunctionURI: this.workFunctionURI,\n uuid: this.uuid,\n mvMultSlicePayment: Number(this.feeStructure.marketValue) || 0, // @todo: improve feeStructure internals to better reflect v4\n absoluteSlicePayment: Number(this.feeStructure.maxPerRequest) || 0,\n requirePath: this.requirePath,\n dependencies: moduleDependencies,\n requirements: this.requirements, /* capex */\n localExec: this.inLocalExec,\n force100pctCPUDensity: this.force100pctCPUDensity,\n estimationSlices: this.estimationSlices,\n greedyEstimation: this.greedyEstimation,\n workerConsole: this.workerConsole,\n isCI: this.isCI,\n\n description: this.public.description || 'Discreetly making the world smarter',\n name: this.public.name || 'Ad-Hoc Job' + adhocId,\n link: this.public.link || '',\n\n preauthToken, // XXXwg/er @todo: validate this after fleshing out the stub(s)\n\n resultStorageType: this.resultStorageType, // @todo: implement other result types\n resultStorageDetails: this.resultStorageDetails, // Content depends on resultStorageType\n resultStorageParams: this.resultStorageParams, // Post params for off-prem storage\n dataRange,\n dataPattern,\n sliceCount,\n marshaledDataValues: this.marshaledDataValues,\n rangeLength: this.rangeLength\n };\n \n // Check if dataRange or dataPattern input is already marshaled\n if (this.marshaledDataRange)\n submitPayload.dataRange = this.marshaledDataRange;\n\n /* Determine composition of argument set and build payload */\n if (this.jobArguments && !this.marshaledArguments)\n submitPayload.marshaledArguments = kvin.marshal(encodeJobValueList(this.jobArguments, 'jobArguments'));\n else\n submitPayload.marshaledArguments = this.marshaledArguments;\n \n // XXXpfr Excellent tracing.\n if (debugging('dcp-client'))\n {\n const { dumpObject } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\n dumpObject(submitPayload, 'Submit: Job Index: submitPayload', 256);\n console.debug('Before Deploy', this.identityKeystore);\n }\n\n // Deploy the job! If we get an error, try again a few times until threshold of errors is reached, then actually throw it\n let deployed\n let deployAttempts = 0;\n while (deployAttempts++ < (dcpConfig.job.deployAttempts || 10))\n {\n try\n {\n deployed = await this.useDeployConnection('submit', submitPayload, this.identityKeystore);\n break;\n }\n catch (e)\n {\n if (deployAttempts < 10)\n debugging('dcp-client') && console.debug('Error when trying to deploy job, trying again', e);\n else\n throw e;\n }\n }\n\n if (!deployed.success)\n {\n // close all of the connections so that we don't cause node processes to hang.\n const handleErr = (e) => {\n console.error('Error while closing job connection:');\n console.error(e);\n };\n \n this.closeDeployConnection();\n this.eventSubscriber.close().catch(handleErr);\n computeGroups.closeServiceConnection().catch(handleErr);\n \n // Yes, it is possible for deployed or deployed.payload to be undefined.\n if (deployed.payload)\n {\n if (deployed.payload.code === 'ENOTFOUND')\n throw new DCPError(`Failed to submit job to scheduler. Account: ${submitPayload.paymentAccount} was not found or does not have sufficient balance (${deployed.payload.info.deployCost} DCCs needed to deploy this job)`, deployed.payload); \n throw new DCPError('Failed to submit job to scheduler', deployed.payload);\n }\n throw new DCPError('Failed to submit job to scheduler (no payload)', deployed ? deployed : '');\n }\n\n debugging('dcp-client') && console.debug('After Deploy', JSON.stringify(deployed));\n\n this.address = deployed.payload.job;\n this.deployCost = deployed.payload.deployCost;\n\n if (!this.status)\n this.status = {\n runStatus: null,\n total: 0,\n computed: 0,\n distributed: 0,\n };\n \n this.status.runStatus = deployed.payload.status;\n this.status.total = deployed.payload.lastSliceNumber;\n this.running = true;\n }\n \n /** close an open job to indicate we are done adding data so it is okay to finish\n * the job at the appropriate time\n */\n close ()\n {\n return this.useDeployConnection('closeJob', {\n job: this.id,\n });\n }\n \n /** Use the connection to job submit service. Will open a new connection if one does not exist,\n * and close the connection if it is idle for more than 10 seconds (tuneable).\n */\n useDeployConnection(...args)\n {\n if (!this.useDeployConnection.uses)\n this.useDeployConnection.uses = 0;\n this.useDeployConnection.uses++;\n if (!this.deployConnection)\n {\n debugging('deploy-connection') && console.info(`1453: making a new deployConnection...`)\n this.deployConnection = new protocolV4.Connection(dcpConfig.scheduler.services.jobSubmit); \n this.deployConnection.on('close', () => { this.deployConnection = null; });\n }\n if (this.deployConnectionTimeout)\n clearTimeout(this.deployConnectionTimeout);\n\n debugging('deploy-connection') && console.info(`1460: sending ${args[0]} request...`);\n const deployPromise = this.deployConnection.send(...args);\n \n deployPromise.finally(() => {\n this.useDeployConnection.uses--;\n\n debugging('deploy-connection') && console.info(`1462: deployConnection done ${args[0]} request, connection uses is ${this.useDeployConnection.uses}`)\n\n this.deployConnectionTimeout = setTimeout(() => {\n if (this.useDeployConnection.uses === 0 && this.deployConnection)\n {\n debugging('deploy-connection') && console.info(`1469: done with deployConn, closing...`);\n // if we're done w/ the connection, then remove its cleanup\n // function, close it, and clean up manually to make room for a\n // new conn when needed (ie, don't leave the closing conn where\n // someone could accidentally pick it up)\n this.deployConnection.removeAllListeners('close');\n this.deployConnection.close();\n this.deployConnection = null;\n }\n }, (dcpConfig.job.deployCloseTimeout || 10 * 1000));\n if (!ON_BROWSER)\n this.deployConnectionTimeout.unref();\n }); \n \n return deployPromise;\n }\n \n /**\n * Close the connection to the job submit (if it exists), and clear the close timeout (if needed).\n */\n closeDeployConnection()\n {\n if (this.deployConnection)\n this.deployConnection.close();\n if (this.deployConnectionTimeout)\n clearTimeout(this.deployConnectionTimeout);\n }\n}\n\n/** \n * Encode a value list for transmission to the job-submit daemon. This could be either job arguments\n * or the input set, if the input set was an Array-like object.\n *\n * @param {ArrayLike} valueList the list of values to encode\n * @returns Array of URIString\n */\nfunction encodeJobValueList(valueList, valueKind)\n{\n var list = [];\n \n /*\n * We need to handle several different styles of datasets, and create the output array accordingly.\n *\n * 1. instance of RemoteDataSet => arguments is a list of URI strings; fetch URIs before handing to work fn\n * 2. an Array-like objects => arguments handed directly to work fn - except instances of RemoteDatum\n * All values sent to the scheduler in payload are sent in their database representation (always as some kind of URI)\n */\n \n if (typeof valueList === 'undefined' || (typeof valueList === 'object' && valueList.length === 0))\n return list; /* empty set */\n\n if (typeof valueList !== 'object' || !valueList.hasOwnProperty('length'))\n throw new Error('value list must be an Array-like object');\n \n for (let i = 0; i < valueList.length; i++) /* Set is composed of values from potentially varying sources */\n {\n let value = valueList[i];\n if (value instanceof RemoteDataSet)\n value.forEach((el) => list.push(new URL(el)));\n else if (value instanceof RemoteDataPattern)\n {\n if (valueKind === jobValueKind.jobArguments)\n throw new DCPError('Cannot use RemoteDataPattern as work function arguments', 'EBADARG');\n else\n {\n let uri = valueList['pattern'];\n for (let sliceNum = 1; sliceNum <= valueList['sliceCount']; sliceNum++)\n list.push(new URL(uri.replace('{slice}', sliceNum)))\n }\n }\n else if (value instanceof RemoteValue)\n list.push(value.href);\n else\n list.push(value);\n } \n\n const encodedList = list.map(encodeJobValueUri)\n return encodedList;\n} \n\n/**\n * Depending on the shape of the job's data, resolve it into a RangeObject, a\n * Pattern, or a values array, and return it in the appropriate property.\n *\n * @param {any} data Job's input data\n * @return {MarshaledInputData} An object with one of the following properties set:\n * - dataValues: job input is an array of arbitrary values \n * - dataPattern: job input is a URI pattern \n * - dataRange: job input is a RangeObject (and/or friends)\n */\nfunction marshalInputData (data)\n{\n if (!(data instanceof Object || data instanceof SuperRangeObject))\n throw new TypeError(`Invalid job data type: ${typeof data}`);\n\n /**\n * @type {MarshaledInputData}\n */\n const marshalledInputData = {};\n\n // TODO(wesgarland): Make this more robust.\n if (data instanceof SuperRangeObject ||\n (data.hasOwnProperty('ranges') && data.ranges instanceof MultiRangeObject) ||\n (data.hasOwnProperty('start') && data.hasOwnProperty('end')))\n marshalledInputData.dataRange = data;\n else if (Array.isArray(data))\n marshalledInputData.dataValues = data;\n else if (data instanceof URL || data instanceof DcpURL)\n marshalledInputData.dataPattern = String(data);\n else if(data instanceof RemoteDataSet)\n marshalledInputData.dataValues = data.map(e => new URL(e));\n else if(data instanceof RemoteDataPattern)\n {\n marshalledInputData.dataPattern = data['pattern'];\n marshalledInputData.sliceCount = data['sliceCount'];\n }\n\n debugging('job') && console.debug('marshalledInputData:', marshalledInputData);\n return marshalledInputData;\n}\n\n/**\n * marshal the value using kvin or instance of the kvin (tunedKvin)\n * tunedKvin is defined if job.tuning.kvin is specified.\n *\n * @param {any} value \n * @return {object} A marshaled object\n * \n */\nfunction kvinMarshal (value) {\n if (tunedKvin)\n return tunedKvin.marshal(value);\n\n return kvin.marshal(value);\n}\n\n\n\nexports.Job = Job;\nexports.SlicePaymentOffer = SlicePaymentOffer;\nexports.ResultHandle = ResultHandle;\n\n\n//# sourceURL=webpack://dcp/./src/dcp-client/job/index.js?");
|
|
4202
|
+
eval("/**\n * @file job/index.js\n * @author Eddie Roosenmaallen, eddie@kingsds.network\n * Matthew Palma, mpalma@kingsds.network\n * Wes Garland, wes@kingsds.network\n * Paul, paul@kingsds.network\n * Ryan Saweczko, ryansaweczko@kingsds.network\n * @date November 2018\n * November 2018\n * February 2022\n * May 2022\n * Jun 2022\n *\n * This module implements the Compute API's Job Handle\n *\n */\n\n\nconst { BigNumber } = __webpack_require__(/*! bignumber.js */ \"./node_modules/bignumber.js/bignumber.js\");\nconst { v4: uuidv4 } = __webpack_require__(/*! uuid */ \"./node_modules/uuid/dist/esm-browser/index.js\");\nconst { EventEmitter, PropagatingEventEmitter } = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\");\nconst { RangeObject, MultiRangeObject, DistributionRange, SuperRangeObject, SparseRangeObject } = __webpack_require__(/*! dcp/dcp-client/range-object */ \"./src/dcp-client/range-object.js\");\nconst { fetchURI, encodeDataURI, createTempFile } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\nconst { encodeJobValueUri, RemoteValue } = __webpack_require__(/*! dcp/dcp-client/job-values */ \"./src/dcp-client/job-values.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 { DcpURL } = __webpack_require__(/*! dcp/common/dcp-url */ \"./src/common/dcp-url.js\");\nconst ClientModal = __webpack_require__(/*! dcp/dcp-client/client-modal */ \"./src/dcp-client/client-modal/index.js\");\nconst { Worker } = __webpack_require__(/*! dcp/dcp-client/worker */ \"./src/dcp-client/worker/index.js\");\nconst { RemoteDataSet } = __webpack_require__(/*! dcp/dcp-client/remote-data-set */ \"./src/dcp-client/remote-data-set.js\");\nconst { RemoteDataPattern } = __webpack_require__(/*! dcp/dcp-client/remote-data-pattern */ \"./src/dcp-client/remote-data-pattern.js\");\nconst { ResultHandle } = __webpack_require__(/*! ./result-handle */ \"./src/dcp-client/job/result-handle.js\");\nconst { SlicePaymentOffer } = __webpack_require__(/*! ./slice-payment-offer */ \"./src/dcp-client/job/slice-payment-offer.js\");\nconst { addSlices } = __webpack_require__(/*! ./upload-slices */ \"./src/dcp-client/job/upload-slices.js\");\nconst DCP_ENV = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\nconst 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 { DEFAULT_REQUIREMENTS, removeBadRequirements } = __webpack_require__(/*! dcp/common/job-requirements-defaults */ \"./src/common/job-requirements-defaults.js\");\nconst { sliceStatus, jobValueKind } = __webpack_require__(/*! dcp/common/scheduler-constants */ \"./src/common/scheduler-constants.js\");\nconst { jobStatus } = __webpack_require__(/*! dcp/common/scheduler-constants */ \"./src/common/scheduler-constants.js\");\nconst bankUtil = __webpack_require__(/*! dcp/dcp-client/bank-util */ \"./src/dcp-client/bank-util.js\");\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\nconst debugging = (__webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope)('dcp-client');\nconst kvin = __webpack_require__(/*! kvin */ \"./node_modules/kvin/kvin.js\");\nlet tunedKvin;\n\nconst log = (...args) => {\n if (debugging('job')) {\n console.debug('dcp-client:job', ...args);\n }\n};\n\nconst ON_BROWSER = DCP_ENV.isBrowserPlatform;\nconst sideloaderModuleIdentifier = 'sideloader-v1';\n\n\n/** @typedef {import('dcp/dcp-client/wallet/keystore').Keystore} Keystore */\n/** @typedef {import('dcp/dcp-client/range-object').RangeLike} RangeLike */\n/** @typedef {import('scheduler-v4/libexec/job-submit/operations/submit').MarshaledInputData} MarshaledInputData */\n\n/**\n * Ensure input data is an appropriate format\n * @param {RangeObject | DistributionRange | RemoteDataSet | Array | Iterable}\n * inputData - A URI-shaped string, a [Multi]RangeObject-constructing value, or\n * an array of slice data\n * @return {RangeObject | RangeLike | DistributionRange | RemoteDataSet | Array}\n * The coerced input in an appropriate format ([Multi]RangeObject,\n * DistributionRange, RemoteDataSet, or array)\n */\n const wrangleData = (inputData) => {\n\n if (RangeObject.isRangelike(inputData)) { return inputData }\n if (RangeObject.isRangeObject(inputData)) { return inputData }\n if (DistributionRange.isDistribution(inputData)) { return inputData }\n if (inputData instanceof SparseRangeObject) { return inputData }\n if (inputData instanceof MultiRangeObject) { return inputData }\n if (MultiRangeObject.isProtoMultiRangelike(inputData)) { return new MultiRangeObject(inputData) }\n if (RangeObject.isProtoRangelike(inputData)) { return new RangeObject(inputData) }\n if (DistributionRange.isProtoDistribution(inputData)) { return new DistributionRange(inputData) }\n if (RemoteDataSet.isRemoteDataSet(inputData)) { return inputData }\n if (RemoteDataPattern.isRemoteDataPattern(inputData)) { return inputData }\n\n return Array.isArray(inputData) ? inputData : [inputData];\n};\n\n/**\n * @classdesc The Compute API's Job Handle (see {@link https://docs.dcp.dev/specs/compute-api.html#job-handles|Compute API spec})\n * Job handles are objects which correspond to jobs. \n * They are created by some exports of the compute module, such as {@link module:dcp/compute.do|compute.do} and {@link module:dcp/compute.for|compute.for}.\n * @extends module:dcp/dcp-events.PropagatingEventEmitter\n * @hideconstructor\n * @access public\n */\nclass Job extends PropagatingEventEmitter\n{\n /**\n * Fired when the job is accepted by the scheduler on deploy.\n * \n * @event Job#accepted\n * @access public\n * @type {object}\n *//**\n * Fired when the job is cancelled.\n * \n * @event Job#cancel\n * @access public\n *//**\n * Fired when a result is returned.\n * \n * @event Job#result\n * @access public\n * @type {object}\n * @property {string} jobAddress Address of the job\n * @property {string} task ID of the task (slice) the result came from\n * @property {number} sort The index of the slice\n * @property {object} result\n * @property {string} result.request\n * @property {*} result.result The value returned from the work function\n *//**\n * Fired when the result handle is modified, either when a new `result` event is fired or when the results are populated with `results.fetch()`\n * \n * @event Job#resultsUpdated\n * @access public\n *//**\n * Fired when the job has been completed.\n * \n * @event Job#complete\n * @access public\n * @type {ResultHandle}\n *//**\n * Fired when the job's status changes.\n * \n * @event Job#status\n * @access public\n * @type {object}\n * @property {string} jobAddress Address of the job\n * @property {number} total Total number of slices in the job\n * @property {number} distributed Number of slices that have been distributed\n * @property {number} computed Number of slices that have completed execution (returned a result)\n * @property {string} runStatus Current runStatus of the job\n *//**\n * Fired when a slice throws an error.\n * \n * @event Job#error\n * @access public\n * @type {object}\n * @property {string} jobAddress Address of the job\n * @property {number} sliceIndex Index of the slice that threw the error\n * @property {string} message The error message\n * @property {string} stack The error stacktrace\n * @property {string} name The error type name\n *//**\n * Fired when a slice uses one of the console log functions.\n * \n * @event Job#console\n * @access public\n * @type {object}\n * @property {string} jobAddress Address of the job\n * @property {number} sliceIndex The index of the slice that produced this event\n * @property {string} level The log level, one of `debug`, `info`, `log`, `warn`, or `error`\n * @property {string} message The console log message\n *//**\n * Fired when a slice is stopped for not calling progress. Contains information about how long the slice ran for, and about the last reported progress calls.\n * \n * @event Job#noProgress\n * @access public\n * @type {object}\n * @property {string} jobAddress Address of the job\n * @property {number} sliceIndex The index of the slice that failed due to no progress\n * @property {number} timestamp How long the slice ran before failing\n * @property {object} progressReports\n * @property {object} progressReports.last The last progress report received from the worker\n * @property {number} progressReports.last.timestamp Time since the start of the slice\n * @property {number} progressReports.last.progress Progress value reported\n * @property {*} progressReports.last.value The last value that was passed to the progress function\n * @property {number} progressReports.last.throttledReports Number of calls to progress that were throttled since the last report\n * @property {object} progressReports.lastUpdate The last determinate (update to the progress param) progress report received from the worker\n * @property {number} progressReports.lastUpdate.timestamp\n * @property {number} progressReports.lastUpdate.progress\n * @property {*} progressReports.lastUpdate.value\n * @property {number} progressReports.lastUpdate.throttledReports\n *//**\n @todo: is this in the spec? is there a no progress data? should there be?\n * Identical to `noProgress`, except that it also contains the data that the slice was executed with.\n * \n * @event Job#noProgressData\n * @access public\n * @type {object}\n * @property {*} data The data that the slice was executed with\n *//**\n * Fired when the job is paused due to running out of funds. The job can be resumed by escrowing more funds then resuming the job.\n * @todo: is this a thing, should it be a thing (the payload)\n * Event payload is the estimated funds required to complete the job\n * \n * @event Job#nofunds\n * @access public\n * @type {BigNumber}\n *//**\n * Fired when the job cannot be deployed due to no bank account / not enough balance to deploy the job\n * \n * @event Job#ENOFUNDS\n * @access public\n *//**\n * Fired when the job is cancelled due to the work function not calling the `progress` method frequently enough.\n * \n * @event Job#ENOPROGRESS\n * @access public\n *//**\n * The job was cancelled because scheduler has determined that individual tasks in this job exceed the maximum allowable execution time.\n * \n * @event Job#ESLICETOOSLOW\n * @access public\n *//**\n * Fired when the job is cancelled because too many work functions are terminating with uncaught exceptions.\n * \n * @event Job#ETOOMANYERRORS\n * @access public\n */\n\n /**\n * @form1 new Job('application_worker_address'[, data[, arguments]])\n * @form2a new Job('worker source'[, data[, arguments]])\n * @form2b new Job(worker_function[, data[, arguments]])\n */\n constructor ()\n {\n super('Job');\n if (typeof arguments[0] === 'function')\n arguments[0] = arguments[0].toString();\n\n if (typeof arguments[0] === 'string')\n {\n const { encodeDataURI } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\n this.workFunctionURI = encodeDataURI(arguments[0], 'application/javascript');\n } \n else if (DcpURL.isURL(arguments[0]))\n this.workFunctionURI = arguments[0].href;\n\n this.jobInputData = wrangleData(arguments[1] || []);\n this.jobArguments = wrangleData(arguments[2] || []);\n \n log('num wrangledInputData:', this.jobInputData.length);\n log('num wrangledArguments:', this.jobArguments.length);\n\n this.initEventSystems();\n\n /**\n * An object describing the cost the user believes each the average slice will incur, in terms of CPU/GPU and I/O.\n * If defined, this object is used to provide initial scheduling hints and to calculate escrow amounts.\n *\n * @type {object}\n * @access public\n */\n this.initialSliceProfile = undefined;\n \n // The max value that the client is willing to spend to deploy\n // (list on the scheduler, doesn't include compute payment)\n // maxDeployPayment is the max the user is willing to pay to DCP (as a\n // Hold), in addition to the per-slice offer and associated scrape.\n // Currently calculated as `deployCost = costPerKB *\n // (JSON.stringify(job).length / 1024) // 1e-9 per kb`\n // @todo: figure this out / er nov 2018\n this.maxDeployPayment = 1;\n\n /**\n * An object describing the requirements that workers must have to be eligible for this job. The default values are set in the job-submitter, and only the client specified\n * requirements are sent over the wire. See {@link https://docs.dcp.dev/specs/compute-api.html#requirements-objects|Requirements Objects}.\n *\n * @type {object}\n * @access public\n */\n this.requirements = {};\n\n /**\n * A place to store public-facing attributes of the job. Anything stored on this object will be available inside the work \n * function (see {@link module:dcp/compute~sandboxEnv.work}). The properties documented here may be used by workers to display what jobs are currently being \n * worked on.\n * @access public\n * @property {string} name Public-facing name of this job.\n * @property {string} description Public-facing description for this job.\n * @property {string} link Public-facing link to external resource about this job.\n */\n this.public = {\n name: null,\n description: null,\n link: null,\n };\n \n /**\n * A cryptographic receipt indicating deployment of the job on the scheduler\n * @type {object}\n * @access public\n */\n this.receipt = null;\n \n /**\n * a SliceProfile object which contains the average costs for the slices which have been computed to date.\n * Until the first result is returned, this property is undefined.\n * @type {object}\n * @access public\n */\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 * When true, allows a job in estimation to have requestTask return multiple estimation slices.\n * This flag applies independent of infinite estimation, viz., this.estimationSlices === null .\n * @type {boolean}\n * @access public\n */\n this.greedyEstimation = false;\n /**\n * tunable parameters per job\n * @access public\n * @param {object} tuning \n * @param {string} tuning.kvin Encode the TypedArray into a string, trying multiple methods to determine optimum \n * size/performance. The this.tune variable affects the behavior of this code this:\n * @param {boolean} speed If true, only do naive encoding: floats get represented as byte-per-digit strings\n * @param {boolean} size If true, try the naive, ab8, and ab16 encodings; pick the smallest\n * If both are false try the naive encoding if under typedArrayPackThreshold and use if smaller\n * than ab8; otherwise, use ab8\n */\n this.tuning = {\n kvin: {\n size: false,\n speed: false,\n },\n }\n /* For API interface to end-users only */\n Object.defineProperty(this, 'id', {\n get: () => this.address,\n set: (id) => { this.address = id }\n });\n \n this.uuid = uuidv4(); /** @see {@link https://kingsds.atlassian.net/browse/DCP-1475?atlOrigin=eyJpIjoiNzg3NmEzOWE0OWI4NGZkNmI5NjU0MWNmZGY2OTYzZDUiLCJwIjoiaiJ9|Jira Issue} */\n this.dependencies = []; /* dependencies of the work function */\n this.requirePath = []; /* require path for dependencies */\n this.modulePath = []; /* path to module that invoked .exec() for job */\n this.connected = false; /* true when exec or resume called */\n this.results = new ResultHandle(this); /* result handle */\n this.collateResults = true; /* option to receive results as they are computed & ensure all are received on finish */\n this.contextId = null; /* optional string which is used to indicate to caching mechanisms different keystores with same name */ \n this.force100pctCPUDensity = false; /* tell scheduler to assume this job uses 100% cpu density */\n this.workerConsole = false; /* tell workers to log more information about failures in the evaluator this job causes */\n this.address = null; /* job address, created by scheduler during exec call. */\n this.paymentAccountKeystore = null; /* keystore for payment for job to come from */\n this.status = { /* job status details */\n runStatus: null,\n total: null,\n distributed: null,\n computed: null\n };\n \n // service locations\n this.scheduler = dcpConfig.scheduler.services.jobSubmit.location;\n this.bank = dcpConfig.bank.services.bankTeller.location;\n \n // Compute groups. Add to public compute group by default\n this.computeGroups = [ Object.assign({}, schedulerConstants.computeGroups.public) ];\n \n // Update the ready state as we go through job deployment\n this.readyState = sliceStatus.new;\n const that = this;\n this.readyStateChange = function job$$readyStateChange (readyState)\n {\n that.readyState = readyState;\n that.emit('readyStateChange', that.readyState);\n }\n }\n \n /**\n * Initialize the various event systems the job handle requires. These include:\n * - an internal event emitter (this.ee)\n * - an event emitter for any events emitted on `work.emit` within work functions (this.work)\n * - an event subscriber to subscribe (to receive) events from the scheduler (this.eventSubscriber)\n */\n initEventSystems ()\n {\n // Handle the various event-related things required in the constructor\n\n // Internal event emitter for events within job handle\n this.ee = new EventEmitter('Job Internal');\n\n /**\n * An EventEmitter for custom events dispatched by the work function.\n * @type {module:dcp/dcp-events.EventEmitter}\n * @access public\n * @example\n * // in work function\n * work.emit('myEventName', 1, [2], \"three\");\n * // client-side\n * job.work.on('myEventName', (num, arr, string) => { });\n */\n this.work = new EventEmitter('job.work');\n this.listenForCustomEvents = false;\n\n // Initialize the eventSubscriber so each job has unique eventSubscriber\n this.eventSubscriber = new ((__webpack_require__(/*! dcp/events/event-subscriber */ \"./src/events/event-subscriber.js\").EventSubscriber))(this);\n\n // Some events from the event subscriber can't be emitted immediately upon receipt without having \n // weird/wrong output due to things like serialization. We allow interceptors in the event subscriber\n // to handle this.\n const that = this\n var lastConsoleEv;\n var sameCounter = 1;\n const parseConsole = function deserializeConsoleMessage(ev) {\n if (tunedKvin)\n ev.message = tunedKvin.unmarshal(ev.message);\n else \n ev.message = kvin.unmarshal(ev.message);\n \n if (lastConsoleEv && ev.message[0] === lastConsoleEv.message[0] && ev.sliceNumber === lastConsoleEv.sliceNumber && ev.level === lastConsoleEv.level)\n ev.same = ++sameCounter;\n else\n sameCounter = 1;\n lastConsoleEv = ev;\n \n /* if we have the same message being logged (same sliceNumber, message, log level), the console event object will have the sole property same, nothing else */\n if (ev.same > 1)\n that.emit('console', { same: ev.same });\n else\n {\n delete ev.same;\n that.emit('console', ev);\n }\n }\n\n this.eventIntercepts = {\n result: (ev) => this.handleResult(ev),\n status: (ev) => this.handleStatus(ev),\n cancel: (ev) => this.ee.emit('stopped', ev),\n custom: (ev) => this.work.emit(ev.customEvent, ev),\n console: parseConsole,\n };\n \n this.eventTypes = (__webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\").eventTypes);\n\n this.work.on('newListener', (evt) => {\n this.listenForCustomEvents = true;\n });\n this.desiredEvents = []\n this.on('newListener', (evt) => {\n if (!this.connected && evt !== 'newListener')\n this.desiredEvents.push(evt);\n if (evt === 'cancel')\n this.listeningForCancel = true;\n });\n }\n \n /** \n * Cancel the job\n * @access public\n * @param {string} reason If provided, will be sent to client\n */\n async cancel (reason = undefined)\n {\n const response = await this.useDeployConnection('cancelJob', {\n job: this.address,\n owner: this.paymentAccountKeystore.address,\n reason,\n }, this.paymentAccountKeystore);\n\n return response.payload;\n }\n\n /** \n * Resume this job\n * @access public\n */\n async resume ()\n {\n const response = await this.schedulerConnection.send('resumeJob', {\n job: this.address,\n owner: this.paymentAccountKeystore.address,\n }, this.paymentAccountKeystore);\n\n return response.payload;\n }\n\n /**\n * Helper function for retrieving info about the job. The job must have already been deployed.\n * An alias for {@link module:dcp/compute.getJobInfo}.\n * @access public\n */\n getJobInfo ()\n {\n return (__webpack_require__(/*! ../compute */ \"./src/dcp-client/compute.js\").compute.getJobInfo)(this.address);\n }\n \n /**\n * Helper function for retrieving info about the job's slices. The job must have already been deployed.\n * An alias for {@link module:dcp/compute.getSliceInfo}.\n * @access public\n */\n getSliceInfo ()\n {\n return (__webpack_require__(/*! ../compute */ \"./src/dcp-client/compute.js\").compute.getSliceInfo)(this.address);\n }\n \n /** Escrow additional funds for this job\n * @access public\n * @param {number|BigNumber} fundsRequired - A number or BigNumber instance representing the funds to escrow for this job\n */\n async escrow (fundsRequired)\n {\n if ((typeof fundsRequired !== 'number' && !BigNumber.isBigNumber(fundsRequired))\n || fundsRequired <= 0 || !Number.isFinite(fundsRequired) || Number.isNaN(fundsRequired))\n throw new Error(`Job.escrow: fundsRequired must be a number greater than zero. (not ${fundsRequired})`);\n\n const bankConnection = new protocolV4.Connection(dcpConfig.bank.services.bankTeller);\n\n /*\n * escrow has been broken for an unknown amount of time. `feeStructureId` is not defined anywhere in the job class, and hasn't\n * for a period of time. When fixed, `this[INTERNAL_SYMBOL].payloadDetails.feeStructureId` will likely become just `this.feeStructureId`,\n * but it's being left alone until someone spends the time to fix escrow. / rs Jul 2022\n */\n const response = await bankConnection.send('embiggenFeeStructure', {\n feeStructureAddress: this[INTERNAL_SYMBOL].payloadDetails.feeStructureId,\n additionalEscrow: new BigNumber(fundsRequired),\n fromAddress: this.paymentAccountKeystore.address,\n }, this.paymentAccountKeystore);\n\n bankConnection.close();\n const escrowReceipt = response.payload;\n return escrowReceipt;\n }\n\n /**\n * create bundles for local dependencies\n */\n _pack ()\n {\n var retval = (__webpack_require__(/*! ./node-modules */ \"./src/dcp-client/job/node-modules.js\").createModuleBundle)(this.dependencies);\n return retval;\n }\n\n /** \n * Collect all of the dependencies together, throw them into a BravoJS\n * module which sideloads them as a side effect of declaration, and transmit\n * them to the package manager. Then we return the package descriptor object,\n * which is guaranteed to have only one file in it.\n *\n * @returns {object} with properties name and files[0]\n */\n async _publishLocalModules()\n {\n const dcpPublish = __webpack_require__(/*! dcp/common/dcp-publish */ \"./src/common/dcp-publish.js\");\n \n const { tempFile, hash, unresolved } = await this._pack();\n\n if (!tempFile) {\n return { unresolved };\n }\n\n const sideloaderFilename = tempFile.filename;\n const pkg = {\n name: `dcp-pkg-v1-localhost-${hash.toString('hex')}`,\n version: '1.0.0',\n files: {\n [sideloaderFilename]: `${sideloaderModuleIdentifier}.js`,\n },\n }\n\n await dcpPublish.publish(pkg);\n tempFile.remove();\n\n return { pkg, unresolved };\n }\n \n /**\n * This function specifies a module dependency (when the argument is a string)\n * or a list of dependencies (when the argument is an array) of the work\n * function. This function can be invoked multiple times before deployment.\n * @param {string | string[]} modulePaths - A string or array describing one\n * or more dependencies of the job.\n * @access public\n */\n requires (modulePaths)\n {\n if (typeof modulePaths !== 'string' && (!Array.isArray(modulePaths) || modulePaths.some((modulePath) => typeof modulePath !== 'string')))\n throw new TypeError('The argument to dependencies is not a string or an array of strings');\n else if (modulePaths.length === 0)\n throw new RangeError('The argument to dependencies cannot be an empty string or array');\n else if (Array.isArray(modulePaths) && modulePaths.some((modulePath) => modulePath.length === 0))\n throw new RangeError('The argument to dependencies cannot be an array containing an empty string');\n\n if (!Array.isArray(modulePaths))\n modulePaths = [modulePaths];\n\n for (const modulePath of modulePaths)\n {\n if (modulePath[0] !== '.' && modulePath.indexOf('/') !== -1)\n {\n const modulePrefixRegEx = /^(.*)\\/.*?$/;\n const [, modulePrefix] = modulePath.match(modulePrefixRegEx);\n if (modulePrefix && this.requirePath.indexOf(modulePrefix) === -1)\n this.requirePath.push(modulePrefix);\n }\n this.dependencies.push(modulePath);\n }\n }\n \n /** Set the account upon which funds will be drawn to pay for the job.\n * @param {module:dcp/wallet.AuthKeystore} keystore A keystore that representa a bank account.\n * @access public\n */\n setPaymentAccountKeystore (keystore)\n {\n if (this.address)\n {\n if (!keystore.address.eq(this.paymentAccountKeystore))\n {\n let message = 'Cannot change payment account after job has been deployed';\n this.emit('EPERM', message);\n throw new Error(`EPERM: ${message}`);\n }\n }\n \n if (!(keystore instanceof wallet.Keystore))\n throw new Error('Not an instance of Keystore: ' + keystore.toString());\n this.paymentAccountKeystore = keystore;\n }\n \n /** Set the slice payment offer. This is equivalent to the first argument to exec.\n * @param {number} slicePaymentOffer - The number of DCC the user is willing to pay to compute one slice of this job\n */\n setSlicePaymentOffer (slicePaymentOffer)\n {\n this.slicePaymentOffer = new SlicePaymentOffer(slicePaymentOffer);\n }\n \n \n /**\n * @param {URL|DcpURL} locationUrl - A URL object\n * @param {object} postParams - An object with any parameters that a user would like to be passed to a \n * remote result location. This object is capable of carry API keys for S3, \n * DropBox, etc. These parameters are passed as parameters in an \n * application/x-www-form-urlencoded request.\n */\n setResultStorage (locationUrl, postParams)\n {\n if (locationUrl instanceof URL || locationUrl instanceof DcpURL)\n this.resultStorageDetails = locationUrl;\n else\n throw new Error('Not an instance of a DCP URL: ' + locationUrl);\n \n\n // resultStorageParams contains any post params required for off-prem storage\n if (typeof postParams !== 'undefined' && typeof postParams === 'object' )\n this.resultStorageParams = postParams;\n else\n throw new Error('Not an instance of a object: ' + postParams);\n\n // Some type of object here\n this.resultStorageType = 'pattern';\n }\n \n /**\n * This function is identical to exec, except that the job is executed locally\n * in the client.\n * @async\n * @param {number} cores - the number of local cores in which to execute the job.\n * @param {...any} args - The remaining arguments are identical to the arguments of exec\n * @return {Promise<ResultHandle>} - resolves with the results of the job, rejects on an error\n * @access public\n */\n async localExec (cores = 1, ...args)\n {\n this.inLocalExec = true;\n this.estimationSlices = 0;\n this.greedyEstimation = false;\n this.isCI = false;\n\n let worker;\n this.on('accepted', () => {\n // Start a worker for this job\n worker = new Worker({\n localExec: true,\n jobAddresses: [this.address],\n allowedOrigins: this.localExecAllowedFiles,\n paymentAddress: this.paymentAccountKeystore.address,\n identity: this.identityKeystore,\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.on('error', (error) => {\n console.error('Worker Error:', error);\n });\n \n worker.on('warning', (warning) => {\n console.warn('Worker Warning:', warning);\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 if (DCP_ENV.platform === 'nodejs')\n {\n this.localExecAllowedFiles =\n {\n any: [],\n fetchData: [],\n fetchWorkFunctions: [],\n fetchArguments: [],\n sendResults: [],\n };\n \n // Determine type of input data\n const { dataRange, dataValues, dataPattern, sliceCount } = marshalInputData(this.jobInputData);\n \n const inputSetFiles = [];\n \n let inputSetURIs = [];\n let dataSet;\n \n if (dataValues)\n {\n for (let i = 0; i < dataValues.length; i++)\n {\n if (!(dataValues[i] instanceof URL))\n {\n let marshaledInputValue = kvinMarshal(dataValues[i]);\n let inputDataFile = createTempFile('dcp-localExec-sliceData-XXXXXXXXX', 'kvin');\n inputDataFile.writeSync(JSON.stringify(marshaledInputValue));\n inputSetFiles.push(inputDataFile);\n inputSetURIs.push(new URL('file://' + inputDataFile.filename));\n }\n else\n {\n inputSetURIs.push(dataValues[i]);\n this.localExecAllowedFiles['fetchData'].push(dataValues[i].origin);\n }\n }\n dataSet = new RemoteDataSet(inputSetURIs);\n if (dataSet.length > 0)\n this.marshaledDataValues = dataSet;\n }\n if (dataRange)\n {\n inputSetFiles.push(createTempFile('dcp-localExec-sliceData-XXXXXXXXX', 'json'));\n let marshaledInputSet = JSON.stringify(dataRange);\n inputSetFiles[0].writeSync(marshaledInputSet)\n inputSetURIs.push(new URL('file://' + inputSetFiles[0].filename));\n dataSet = new RemoteDataSet(inputSetURIs);\n this.marshaledDataRange = dataSet;\n this.rangeLength = dataRange.length;\n }\n \n if (dataPattern)\n {\n let uri = dataPattern;\n for (let i = 0; i < sliceCount; i++)\n {\n let sliceNum = i+1;\n let newURI = new URL(uri.replace('{slice}', sliceNum.toString()));\n this.localExecAllowedFiles['fetchData'].push(newURI.origin);\n }\n }\n \n // For allowed origins of the localexec worker. Only allow the origins (files in this case) in this list.\n for (let i = 0; i < inputSetFiles.length; i++)\n this.localExecAllowedFiles['fetchData'].push(inputSetFiles[i].filename);\n \n // Save work function to disk if work function starts with data (ie not remote)\n if (this.workFunctionURI.startsWith('data:'))\n {\n const workFunctionFile = createTempFile('dcp-localExec-workFunction-XXXXXXXXX', 'js');\n const workFunction = await fetchURI(this.workFunctionURI);\n workFunctionFile.writeSync(workFunction);\n \n const workFunctionFileURL = new URL('file://' + workFunctionFile);\n this.workFunctionURI = workFunctionFileURL.href;\n this.localExecAllowedFiles['fetchWorkFunctions'].push(workFunctionFile.filename);\n }\n else\n this.localExecAllowedFiles['fetchWorkFunctions'].push(new URL(this.workFunctionURI).origin);\n \n let encodedJobArgumentUris = [];\n if (this.jobArguments)\n {\n if (this.jobArguments instanceof RemoteDataPattern) /* Not supported */\n throw new DCPError('Cannot use RemoteDataPattern as work function arguments', 'EBADARG')\n if (this.jobArguments instanceof RemoteDataSet) /* Entire set is RemoteDataSet */\n {\n this.jobArguments.forEach((e) =>\n {\n this.localExecAllowedFiles['fetchArguments'].push(new URL(e).origin);\n encodedJobArgumentUris.push(encodeJobValueUri(new URL(e)));\n });\n }\n else\n {\n for (let i = 0; i < this.jobArguments.length; i++)\n {\n if (this.jobArguments[i] instanceof URL)\n {\n this.localExecAllowedFiles['fetchArguments'].push(this.jobArguments[i].origin);\n encodedJobArgumentUris.push(encodeJobValueUri(this.jobArguments[i]));\n }\n else\n {\n if (this.jobArguments[i] instanceof RemoteDataSet) /* Member of set is RemoteDataSet */\n {\n this.jobArguments[i].forEach((e) =>\n {\n this.localExecAllowedFiles['fetchArguments'].push(new URL(e).origin);\n encodedJobArgumentUris.push(encodeJobValueUri(new URL(e)));\n });\n }\n else /* Actual Value */\n {\n const localArgFile = createTempFile(`dcp-localExec-argument-${i}-XXXXXXXXX`, 'kvin');\n localArgFile.writeSync(JSON.stringify(kvinMarshal(this.jobArguments[i])));\n this.localExecAllowedFiles['fetchArguments'].push(localArgFile.filename);\n encodedJobArgumentUris.push(encodeJobValueUri(new URL('file://' + localArgFile.filename)));\n }\n }\n } \n }\n }\n this.marshaledArguments = kvinMarshal(encodedJobArgumentUris);\n }\n \n return this.exec(...args).finally(() => {\n if (worker) {\n setTimeout(() => {\n // stop the worker\n worker.stop(true);\n }, 3000);\n }\n });\n }\n\n /**\n * Deploys the job to the scheduler.\n * @param {number | object} [slicePaymentOffer=compute.marketValue] - Amount\n * in DCC that the user is willing to pay per slice.\n * @param {Keystore} [paymentAccountKeystore=wallet.get] - An instance of the\n * Wallet API Keystore that's used as the payment account when executing the\n * job.\n * @param {object} [initialSliceProfile] - An object describing the cost the\n * user believes the average slice will incur.\n * @access public\n * @emits Job#accepted\n */\n async exec (slicePaymentOffer = (__webpack_require__(/*! ../compute */ \"./src/dcp-client/compute.js\").compute.marketValue), paymentAccountKeystore, initialSliceProfile)\n {\n if (this.connected)\n throw new Error('Exec called twice on the same job handle.');\n \n if (this.estimationSlices === Infinity)\n this.estimationSlices = null;\n else if (this.estimationSlices < 0)\n throw new Error('Incorrect value for estimationSlices; it can be an integer or Infinity!');\n \n if (this.tuning.kvin.speed || this.tuning.kvin.size)\n {\n tunedKvin = new kvin.KVIN();\n tunedKvin.tune = 'size';\n if(this.tuning.kvin.speed)\n tunedKvin.tune = 'speed';\n // If both size and speed are true, kvin will optimize based on speed\n if(this.tuning.kvin.speed && this.tuning.kvin.size)\n console.log('Slices and arguments are being uploaded with speed optimization.');\n }\n \n /* slight optimization to ensure we don't send requirements that will be ignored in the job submitter. Make a copy of the client specified requirements for this so that we dont magically override something they manually set */\n const _DEFAULT_REQUIREMENTS = JSON.parse(JSON.stringify(DEFAULT_REQUIREMENTS));\n removeBadRequirements(this.requirements, _DEFAULT_REQUIREMENTS);\n \n this.readyStateChange('exec');\n if ((typeof slicePaymentOffer === 'number') || (typeof slicePaymentOffer === 'object')\n || ((this.slicePaymentOffer === null || this.slicePaymentOffer === undefined) && typeof slicePaymentOffer === 'function'))\n this.setSlicePaymentOffer(slicePaymentOffer);\n if (typeof initialSliceProfile !== 'undefined')\n this.initialSliceProfile = initialSliceProfile;\n \n if (typeof paymentAccountKeystore !== 'undefined')\n {\n /** XXX @todo deprecate use of ethereum wallet objects */\n if (typeof paymentAccountKeystore === 'object' && paymentAccountKeystore.hasOwnProperty('_privKey'))\n {\n console.warn('* deprecated API * - job.exec invoked with ethereum wallet object as paymentAccountKeystore') /* /wg oct 2019 */\n paymentAccountKeystore = paymentAccountKeystore._privKey\n }\n /** XXX @todo deprecate use of private keys */\n if (wallet.isPrivateKey(paymentAccountKeystore))\n {\n console.warn('* deprecated API * - job.exec invoked with private key as paymentAccountKeystore') /* /wg dec 2019 */\n paymentAccountKeystore = await new wallet.Keystore(paymentAccountKeystore, '');\n }\n\n this.setPaymentAccountKeystore(paymentAccountKeystore)\n }\n \n if (this.paymentAccountKeystore)\n // Throws if they fail to unlock, we allow this since the keystore was set programmatically. \n await this.paymentAccountKeystore.unlock(undefined, parseFloat(dcpConfig.job.maxDeployTime));\n else\n {\n // If not set programmatically, we keep trying to get an unlocked keystore ... forever.\n let locked = true;\n let safety = 0; // no while loop shall go unguarded\n let ks;\n do\n {\n ks = null;\n // custom message for the browser modal to denote the purpose of keystore submission\n let msg = `This application is requesting a keystore file to execute ${this.public.description || this.public.name || 'this job'}. Please upload the corresponding keystore file. If you upload a keystore file which has been encrypted with a passphrase, the application will not be able to use it until it prompts for a passphrase and you enter it.`;\n try\n {\n ks = await wallet.get({ contextId: this.contextId, jobName: this.public.name, msg});\n }\n catch (e)\n {\n if (e.code !== ClientModal.CancelErrorCode) throw e;\n };\n if (ks)\n {\n try\n {\n await ks.unlock(undefined, parseFloat(dcpConfig.job.maxDeployTime));\n locked = false;\n }\n catch (e)\n {\n // prompt user again if user enters password incorrectly, exit modal otherwise\n if (e.code !== wallet.unlockFailErrorCode) throw e;\n }\n }\n if (safety++ > 1000) throw new Error('EINFINITY: job.exec tried wallet.get more than 1000 times.')\n } while (locked);\n this.setPaymentAccountKeystore(ks)\n }\n \n // We either have a valid keystore + password or we have rejected by this point.\n if (!this.slicePaymentOffer)\n throw new Error('A payment profile must be assigned before executing the job');\n else\n this.feeStructure = this.slicePaymentOffer.toFeeStructure(this.jobInputData.length);\n\n if (!this.address)\n {\n try\n {\n this.readyStateChange('init');\n await this.deployJob();\n const listenersPromise = this.addInitialEvents();\n const computeGroupsPromise = this.joinComputeGroups();\n let uploadSlicePromise;\n // if job data is by value then upload data to the scheduler in a staggered fashion\n if (Array.isArray(this.dataValues) && !this.marshaledDataValues)\n {\n this.readyStateChange('uploading');\n uploadSlicePromise = addSlices(this.dataValues, this.address, tunedKvin)\n .then(() => {\n debugging('slice-upload') && console.info(`970: slice data uploaded, closing job...`);\n return this.close();\n });\n }\n \n // await all promises for operations that can be done after the job is deployed\n await Promise.all([listenersPromise, computeGroupsPromise, uploadSlicePromise]);\n \n this.readyStateChange('deployed');\n this.emit('accepted', { job: this });\n }\n catch (error)\n {\n if (ON_BROWSER)\n await ClientModal.alert(error, { title: 'Failed to deploy job!' });\n throw error;\n }\n }\n else\n {\n // reconnecting to an old job\n await this.addInitialEvents();\n this.readyStateChange('reconnected');\n }\n\n this.connected = true;\n\n return new Promise((resolve, reject) => {\n const onComplete = () => resolve(this.results);\n const onCancel = (event) => {\n /**\n * FIXME(DCP-1150): Remove this since normal cancel event is noisy\n * enough to not need stopped event too.\n */\n if (ON_BROWSER && !this.listeningForCancel)\n ClientModal.alert('More details in console...', { title: 'Job Canceled' });\n this.emit('cancel', event);\n\n let errorMsg = event.reason;\n if (event.error && event.error !== 'undefined')\n errorMsg = errorMsg +`\\n Recent error message: ${event.error.message}`\n \n reject(new DCPError(errorMsg, event.code));\n };\n\n this.ee.once('stopped', async (stopEvent) => {\n // There is a chance the result submitter will emit finished > 1 time. Only handle it once.\n if (this.receivedStop)\n return;\n this.receivedStop = true;\n this.emit('stopped', stopEvent.runStatus);\n switch (stopEvent.runStatus) {\n case jobStatus.finished:\n if (this.collateResults)\n {\n let report = await this.getJobInfo();\n let allSliceNumbers = Array.from(Array(report.totalSlices)).map((e,i)=>i+1);\n let remainSliceNumbers = allSliceNumbers.filter((e) => !this.results.isAvailable(e));\n\n if (remainSliceNumbers.length)\n {\n const promises = remainSliceNumbers.map(sliceNumber => this.results.fetch([sliceNumber], true));\n await Promise.all(promises);\n }\n }\n\n this.emit('complete', this.results);\n onComplete();\n break;\n case jobStatus.cancelled:\n onCancel(stopEvent);\n break;\n default:\n /**\n * Asserting that we should never be able to reach here. The only\n * scheduler events that should trigger the Job's 'stopped' event\n * are jobStatus.cancelled, jobStatus.finished, and sliceStatus.paused.\n */\n reject(new Error(`Unknown event \"${stopEvent.runStatus}\" caused the job to be stopped.`));\n break;\n }\n });\n\n }).finally(() => {\n const handleErr = (e) => {\n console.error('Error while closing job connection:');\n console.error(e);\n }\n\n // Create an async IIFE to not block the promise chain\n (async () => {\n // delay to let last few events to be received\n await new Promise((resolve) => setTimeout(resolve, 1000));\n \n // close all of the connections so that we don't cause node processes to hang.\n this.closeDeployConnection();\n await this.eventSubscriber.close().catch(handleErr);\n await computeGroups.closeServiceConnection().catch((err) => {\n console.error('Warning: could not close compute groups service connection', err);\n });\n })();\n });\n }\n \n /**\n * job.addListeners(): Private function used to set up event listeners to the scheduler\n * before deploying the job.\n */\n async addInitialEvents ()\n {\n this.readyStateChange('listeners');\n\n // This is important: We need to flush the task queue before adding listeners\n // because we queue pending listeners by listening to the newListener event (in the constructor).\n // If we don't flush here, then the newListener events may fire after this function has run,\n // and the events won't be properly set up.\n await new Promise(resolve => setTimeout(resolve, 0));\n\n // @todo: Listen for an estimated cost, probably emit an \"estimated\" event when it comes in?\n // also @todo: Do the estimation task(s) on the scheduler and send an \"estimated\" event\n\n // Always listen to the stop event. It will resolve the work function promise, so is always needed.\n this.on('stop', (ev) => {this.ee.emit('stopped', ev)});\n\n // Connect listeners that were set up before exec\n if (this.desiredEvents.includes('result'))\n this.listeningForResults = true;\n await this.subscribeNewEvents(this.desiredEvents);\n\n // Connect listeners that are set up after exec\n this.on('newListener', (evt) => {\n if (evt === 'newListener' || this.desiredEvents.includes(evt))\n return;\n this.subscribeNewEvents([evt]);\n });\n \n // automatically add a listener for results if collateResults is on\n if (this.collateResults && !this.listeningForResults)\n this.on('result', () => {});\n\n debugging('dcp-client') && console.debug('subscribedEvents', this.desiredEvents);\n\n // If we have listeners for job.work, subscribe to custom events\n if (this.listenForCustomEvents)\n await this.subscribeCustomEvents();\n // Connect work event listeners that are set up after exec\n else\n this.work.on('newListener', () => this.subscribeCustomEvents());\n }\n \n /**\n * Subscribes to either reliable events or optional events. It is assumed that\n * any call to this function will include only new events.\n * @param {string[]} events \n */\n async subscribeNewEvents (events)\n {\n const reliableEvents = [];\n const optionalEvents = [];\n for (let eventName of events)\n {\n eventName = eventName.toLowerCase();\n if (this.eventTypes[eventName] && this.eventTypes[eventName].reliable)\n reliableEvents.push(eventName);\n else if (this.eventTypes[eventName] && !this.eventTypes[eventName].reliable)\n optionalEvents.push(eventName);\n else\n debugging('dcp-client') && console.debug(`Job handler has listener ${eventName} which isn't an event-router event.`);\n }\n if (debugging('dcp-client'))\n {\n console.debug('reliableEvents', reliableEvents);\n console.debug('optionalEvents', optionalEvents);\n }\n await this.eventSubscriber.subscribeManyEvents(reliableEvents, optionalEvents, { filter: { job: this.address } });\n }\n \n /**\n * Establishes listeners for worker events when requested by the client\n */\n async subscribeCustomEvents ()\n {\n if (!this.listeningForCustomEvents)\n await this.eventSubscriber.subscribeManyEvents([], ['custom'], { filter: { job: this.address } });\n this.listeningForCustomEvents = true\n }\n \n async joinComputeGroups ()\n {\n // localExec jobs are not entered in any compute group.\n if (!this.inLocalExec && this.computeGroups && this.computeGroups.length > 0)\n {\n this.readyStateChange('compute-groups');\n computeGroups.addRef(); // Just in case we're doing a Promise.all on multiple execs.\n\n // Add this job to its currently-defined compute groups (as well as public group, if included)\n let success;\n \n if (!Array.isArray(this.computeGroups)) \n throw new DCPError('Compute groups must be wrapped in an Array', 'DCPL-1101');\n\n for (let i = 0; i < this.computeGroups.length; i++)\n {\n let value = this.computeGroups[i];\n \n if (typeof value !== 'object')\n throw new DCPError(`This compute group: ${value[i]} must be an object`, 'DCPL-1102');\n \n if (value.joinKey && typeof value.joinKey !== 'string' && !(value.joinKey instanceof String))\n throw new DCPError(`This join key: ${value.joinKey} must be a string or a string literal`, 'DCPL-1103');\n else if (value.joinKeystore && !(value.joinKeystore instanceof wallet.Keystore))\n throw new DCPError(`This join Keystore: ${value.joinKeystore} must be an instance of wallet.Keystore`, 'DCPL-1104');\n else if (!value.joinKey && !value.joinKeystore)\n throw new DCPError('Compute group must contain a joinKey or a joinKeystore', 'DCPL-1105');\n }\n \n try\n {\n const cgPayload = await computeGroups.addJobToGroups(this.address, this.computeGroups);\n success = true; // To support older version of CG service where addJobToGroups had void/undefined return.\n if (cgPayload) success = cgPayload.success;\n debugging('dcp-client') && console.debug('job/index: addJobToGroups cgPayload:', cgPayload ? cgPayload : 'cgPayload is not defined; probably from legacy CG service.');\n }\n catch (e)\n {\n debugging('dcp-client') && console.debug('job/index: addJobToGroups threw exception:', e);\n success = false;\n }\n\n computeGroups.closeServiceConnection().catch((err) => {\n console.error('Warning: could not close compute groups service connection', err)\n });\n\n /* Could not put the job in any compute group, even though the user wanted it to run. Cancel the job. */\n if (!success)\n {\n await this.cancel('compute-groups::Unable to join any compute groups');\n throw new DCPError(`Access Denied::Failed to add job ${this.address} to any of the desired compute groups`, 'DCPL-1100');\n }\n }\n }\n \n /**\n * Takes result events as input, stores the result and fires off\n * events on the job handle as required. (result, duplicate-result)\n *\n * @param {object} ev - the event recieved from protocol.listen('/results/0xThisGenAdr')\n */\n async handleResult (ev)\n {\n if (this.results === null)\n // This should never happen - the onResult event should only be established/called\n // in addListeners which should also initialize the internal results array\n throw new Error('Job.onResult was invoked before initializing internal results');\n \n const { result: _result, time } = ev.result;\n debugging('dcp-client') && console.debug('handleResult', _result);\n let result = await fetchURI(_result);\n\n if (this.results.isAvailable(ev.sliceNumber))\n {\n const changed = JSON.stringify(this.results[ev.sliceNumber]) !== JSON.stringify(result);\n this.emit('duplicate-result', { sliceNumber: ev.sliceNumber, changed });\n }\n\n this.results.newResult(result, ev.sliceNumber);\n }\n \n /**\n * Receives status events from the scheduler, updates the local status object\n * and emits a 'status' event\n *\n * @param {object} ev - the status event received from\n * protocol.listen('/status/0xThisGenAdr')\n * @param {boolean} emitStatus - value indicating whether or not the status\n * event should be emitted\n */\n handleStatus ({ runStatus, total, distributed, computed }, emitStatus = true)\n {\n Object.assign(this.status, {\n runStatus,\n total,\n distributed,\n computed,\n });\n\n if (emitStatus)\n this.emit('status', { ...this.status, job: this.address });\n }\n \n /**\n * Sends a request to the scheduler to deploy the job.\n */\n async deployJob ()\n {\n var moduleDependencies; \n \n /* Send sideloader bundle to the package server */\n if (DCP_ENV.platform === 'nodejs' && this.dependencies.length)\n {\n try\n {\n let { pkg, unresolved } = await this._publishLocalModules();\n\n moduleDependencies = unresolved;\n if (pkg)\n moduleDependencies.push(pkg.name + '/' + sideloaderModuleIdentifier); \n }\n catch(error)\n {\n throw new DCPError(`Error trying to communicate with package manager server: ${error}`);\n }\n }\n else\n moduleDependencies = this.dependencies;\n \n this.readyStateChange('preauth');\n\n const adhocId = this.uuid.slice(this.uuid.length - 6, this.uuid.length);\n const schedId = await dcpConfig.scheduler.identity;\n // The following check is needed for when using dcp-rtlink and loading the config through source, instead of using the dcp-client bundle\n let schedIdAddress = schedId;\n if(schedId.address)\n schedIdAddress = schedId.address;\n this.identityKeystore = await wallet.getId();\n const preauthToken = await bankUtil.preAuthorizePayment(schedIdAddress, this.maxDeployPayment, this.paymentAccountKeystore);\n const { dataRange, dataValues, dataPattern, sliceCount } = marshalInputData(this.jobInputData);\n if(dataValues)\n this.dataValues = dataValues;\n\n this.readyStateChange('deploying');\n\n /* Payload format is documented in scheduler-v4/libexec/job-submit/operations/submit.js */\n const submitPayload = {\n owner: this.identityKeystore.address,\n paymentAccount: this.paymentAccountKeystore.address,\n priority: 0, // @nyi\n\n workFunctionURI: this.workFunctionURI,\n uuid: this.uuid,\n mvMultSlicePayment: Number(this.feeStructure.marketValue) || 0, // @todo: improve feeStructure internals to better reflect v4\n absoluteSlicePayment: Number(this.feeStructure.maxPerRequest) || 0,\n requirePath: this.requirePath,\n dependencies: moduleDependencies,\n requirements: this.requirements, /* capex */\n localExec: this.inLocalExec,\n force100pctCPUDensity: this.force100pctCPUDensity,\n estimationSlices: this.estimationSlices,\n greedyEstimation: this.greedyEstimation,\n workerConsole: this.workerConsole,\n isCI: this.isCI,\n\n description: this.public.description || 'Discreetly making the world smarter',\n name: this.public.name || 'Ad-Hoc Job' + adhocId,\n link: this.public.link || '',\n\n preauthToken, // XXXwg/er @todo: validate this after fleshing out the stub(s)\n\n resultStorageType: this.resultStorageType, // @todo: implement other result types\n resultStorageDetails: this.resultStorageDetails, // Content depends on resultStorageType\n resultStorageParams: this.resultStorageParams, // Post params for off-prem storage\n dataRange,\n dataPattern,\n sliceCount,\n marshaledDataValues: this.marshaledDataValues,\n rangeLength: this.rangeLength\n };\n \n // Check if dataRange or dataPattern input is already marshaled\n if (this.marshaledDataRange)\n submitPayload.dataRange = this.marshaledDataRange;\n\n /* Determine composition of argument set and build payload */\n if (this.jobArguments && !this.marshaledArguments)\n submitPayload.marshaledArguments = kvin.marshal(encodeJobValueList(this.jobArguments, 'jobArguments'));\n else\n submitPayload.marshaledArguments = this.marshaledArguments;\n \n // XXXpfr Excellent tracing.\n if (debugging('dcp-client'))\n {\n const { dumpObject } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\n dumpObject(submitPayload, 'Submit: Job Index: submitPayload', 256);\n console.debug('Before Deploy', this.identityKeystore);\n }\n\n // Deploy the job! If we get an error, try again a few times until threshold of errors is reached, then actually throw it\n let deployed\n let deployAttempts = 0;\n while (deployAttempts++ < (dcpConfig.job.deployAttempts || 10))\n {\n try\n {\n deployed = await this.useDeployConnection('submit', submitPayload, this.identityKeystore);\n break;\n }\n catch (e)\n {\n if (deployAttempts < 10)\n debugging('dcp-client') && console.debug('Error when trying to deploy job, trying again', e);\n else\n throw e;\n }\n }\n\n if (!deployed.success)\n {\n // close all of the connections so that we don't cause node processes to hang.\n const handleErr = (e) => {\n console.error('Error while closing job connection:');\n console.error(e);\n };\n \n this.closeDeployConnection();\n this.eventSubscriber.close().catch(handleErr);\n computeGroups.closeServiceConnection().catch(handleErr);\n \n // Yes, it is possible for deployed or deployed.payload to be undefined.\n if (deployed.payload)\n {\n if (deployed.payload.code === 'ENOTFOUND')\n throw new DCPError(`Failed to submit job to scheduler. Account: ${submitPayload.paymentAccount} was not found or does not have sufficient balance (${deployed.payload.info.deployCost} DCCs needed to deploy this job)`, deployed.payload); \n throw new DCPError('Failed to submit job to scheduler', deployed.payload);\n }\n throw new DCPError('Failed to submit job to scheduler (no payload)', deployed ? deployed : '');\n }\n\n debugging('dcp-client') && console.debug('After Deploy', JSON.stringify(deployed));\n\n this.address = deployed.payload.job;\n this.deployCost = deployed.payload.deployCost;\n\n if (!this.status)\n this.status = {\n runStatus: null,\n total: 0,\n computed: 0,\n distributed: 0,\n };\n \n this.status.runStatus = deployed.payload.status;\n this.status.total = deployed.payload.lastSliceNumber;\n this.running = true;\n }\n \n /** close an open job to indicate we are done adding data so it is okay to finish\n * the job at the appropriate time\n */\n close ()\n {\n return this.useDeployConnection('closeJob', {\n job: this.id,\n });\n }\n \n /** Use the connection to job submit service. Will open a new connection if one does not exist,\n * and close the connection if it is idle for more than 10 seconds (tuneable).\n */\n useDeployConnection(...args)\n {\n if (!this.useDeployConnection.uses)\n this.useDeployConnection.uses = 0;\n this.useDeployConnection.uses++;\n if (!this.deployConnection)\n {\n debugging('deploy-connection') && console.info(`1453: making a new deployConnection...`)\n this.deployConnection = new protocolV4.Connection(dcpConfig.scheduler.services.jobSubmit); \n this.deployConnection.on('close', () => { this.deployConnection = null; });\n }\n if (this.deployConnectionTimeout)\n clearTimeout(this.deployConnectionTimeout);\n\n debugging('deploy-connection') && console.info(`1460: sending ${args[0]} request...`);\n const deployPromise = this.deployConnection.send(...args);\n \n deployPromise.finally(() => {\n this.useDeployConnection.uses--;\n\n debugging('deploy-connection') && console.info(`1462: deployConnection done ${args[0]} request, connection uses is ${this.useDeployConnection.uses}`)\n\n this.deployConnectionTimeout = setTimeout(() => {\n if (this.useDeployConnection.uses === 0 && this.deployConnection)\n {\n debugging('deploy-connection') && console.info(`1469: done with deployConn, closing...`);\n // if we're done w/ the connection, then remove its cleanup\n // function, close it, and clean up manually to make room for a\n // new conn when needed (ie, don't leave the closing conn where\n // someone could accidentally pick it up)\n this.deployConnection.removeAllListeners('close');\n this.deployConnection.close();\n this.deployConnection = null;\n }\n }, (dcpConfig.job.deployCloseTimeout || 10 * 1000));\n if (!ON_BROWSER)\n this.deployConnectionTimeout.unref();\n }); \n \n return deployPromise;\n }\n \n /**\n * Close the connection to the job submit (if it exists), and clear the close timeout (if needed).\n */\n closeDeployConnection()\n {\n if (this.deployConnection)\n this.deployConnection.close();\n if (this.deployConnectionTimeout)\n clearTimeout(this.deployConnectionTimeout);\n }\n}\n\n/** \n * Encode a value list for transmission to the job-submit daemon. This could be either job arguments\n * or the input set, if the input set was an Array-like object.\n *\n * @param {ArrayLike} valueList the list of values to encode\n * @returns Array of URIString\n */\nfunction encodeJobValueList(valueList, valueKind)\n{\n var list = [];\n \n /*\n * We need to handle several different styles of datasets, and create the output array accordingly.\n *\n * 1. instance of RemoteDataSet => arguments is a list of URI strings; fetch URIs before handing to work fn\n * 2. an Array-like objects => arguments handed directly to work fn - except instances of RemoteDatum\n * All values sent to the scheduler in payload are sent in their database representation (always as some kind of URI)\n */\n \n if (typeof valueList === 'undefined' || (typeof valueList === 'object' && valueList.length === 0))\n return list; /* empty set */\n\n if (typeof valueList !== 'object' || !valueList.hasOwnProperty('length'))\n throw new Error('value list must be an Array-like object');\n \n for (let i = 0; i < valueList.length; i++) /* Set is composed of values from potentially varying sources */\n {\n let value = valueList[i];\n if (value instanceof RemoteDataSet)\n value.forEach((el) => list.push(new URL(el)));\n else if (value instanceof RemoteDataPattern)\n {\n if (valueKind === jobValueKind.jobArguments)\n throw new DCPError('Cannot use RemoteDataPattern as work function arguments', 'EBADARG');\n else\n {\n let uri = valueList['pattern'];\n for (let sliceNum = 1; sliceNum <= valueList['sliceCount']; sliceNum++)\n list.push(new URL(uri.replace('{slice}', sliceNum)))\n }\n }\n else if (value instanceof RemoteValue)\n list.push(value.href);\n else\n list.push(value);\n } \n\n const encodedList = list.map(encodeJobValueUri)\n return encodedList;\n} \n\n/**\n * Depending on the shape of the job's data, resolve it into a RangeObject, a\n * Pattern, or a values array, and return it in the appropriate property.\n *\n * @param {any} data Job's input data\n * @return {MarshaledInputData} An object with one of the following properties set:\n * - dataValues: job input is an array of arbitrary values \n * - dataPattern: job input is a URI pattern \n * - dataRange: job input is a RangeObject (and/or friends)\n */\nfunction marshalInputData (data)\n{\n if (!(data instanceof Object || data instanceof SuperRangeObject))\n throw new TypeError(`Invalid job data type: ${typeof data}`);\n\n /**\n * @type {MarshaledInputData}\n */\n const marshalledInputData = {};\n\n // TODO(wesgarland): Make this more robust.\n if (data instanceof SuperRangeObject ||\n (data.hasOwnProperty('ranges') && data.ranges instanceof MultiRangeObject) ||\n (data.hasOwnProperty('start') && data.hasOwnProperty('end')))\n marshalledInputData.dataRange = data;\n else if (Array.isArray(data))\n marshalledInputData.dataValues = data;\n else if (data instanceof URL || data instanceof DcpURL)\n marshalledInputData.dataPattern = String(data);\n else if(data instanceof RemoteDataSet)\n marshalledInputData.dataValues = data.map(e => new URL(e));\n else if(data instanceof RemoteDataPattern)\n {\n marshalledInputData.dataPattern = data['pattern'];\n marshalledInputData.sliceCount = data['sliceCount'];\n }\n\n debugging('job') && console.debug('marshalledInputData:', marshalledInputData);\n return marshalledInputData;\n}\n\n/**\n * marshal the value using kvin or instance of the kvin (tunedKvin)\n * tunedKvin is defined if job.tuning.kvin is specified.\n *\n * @param {any} value \n * @return {object} A marshaled object\n * \n */\nfunction kvinMarshal (value) {\n if (tunedKvin)\n return tunedKvin.marshal(value);\n\n return kvin.marshal(value);\n}\n\n\n\nexports.Job = Job;\nexports.SlicePaymentOffer = SlicePaymentOffer;\nexports.ResultHandle = ResultHandle;\n\n\n//# sourceURL=webpack://dcp/./src/dcp-client/job/index.js?");
|
|
4203
4203
|
|
|
4204
4204
|
/***/ }),
|
|
4205
4205
|
|
|
@@ -4300,7 +4300,7 @@ eval("/* provided dependency */ var process = __webpack_require__(/*! ./node_mod
|
|
|
4300
4300
|
\*************************************************/
|
|
4301
4301
|
/***/ ((module, __unused_webpack_exports, __webpack_require__) => {
|
|
4302
4302
|
|
|
4303
|
-
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=
|
|
4303
|
+
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=bcdd0728ce28e9f58c7093651f0113bf0221c93a,' + Date.now() + hash;\n\n window.location.replace(newUrl);\n }\n}\n\nObject.assign(module.exports, {\n SchedMsgWeb\n});\n\n\n//# sourceURL=webpack://dcp/./src/dcp-client/schedmsg/schedmsg-web.js?");
|
|
4304
4304
|
|
|
4305
4305
|
/***/ }),
|
|
4306
4306
|
|
|
@@ -4445,7 +4445,7 @@ eval("/**\n * @file node-localExec.js Node-specific support for cre
|
|
|
4445
4445
|
/***/ ((__unused_webpack_module, exports, __webpack_require__) => {
|
|
4446
4446
|
|
|
4447
4447
|
"use strict";
|
|
4448
|
-
eval("/**\n * @file This module implements the Worker API, used to create workers for earning DCCs.\n * @author Ryan Rossiter <ryan@kingsds.network>\n * Paul <paul@kingsds.network>\n * @date May 2020\n * June, July 2022\n * \n * @module dcp/worker\n * @access public\n */\n// @ts-check\n\n\nconst debugging = (__webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope)('worker');\nconst { SchedMsg } = __webpack_require__(/*! dcp/dcp-client/schedmsg */ \"./src/dcp-client/schedmsg/index.js\");\nconst { EventEmitter } = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\");\nconst { localStorage } = __webpack_require__(/*! dcp/common/dcp-localstorage */ \"./src/common/dcp-localstorage.js\");\nconst { confirmPrompt } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\nconst { assert } = __webpack_require__(/*! dcp/common/dcp-assert */ \"./src/common/dcp-assert.js\");\nconst { Keystore, Address } = __webpack_require__(/*! dcp/dcp-client/wallet */ \"./src/dcp-client/wallet/index.js\");\n\n\n// To use Supervisor2 set the environment variable `USE_SUPERVISOR2`.\nconst USE_SUPERVISOR2 = Boolean((__webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\").getenv)('USE_SUPERVISOR2'));\nconst { Supervisor } = USE_SUPERVISOR2 ? __webpack_require__(/*! ./supervisor2 */ \"./src/dcp-client/worker/supervisor2/index.js\") : __webpack_require__(/*! ./supervisor */ \"./src/dcp-client/worker/supervisor.js\");\n\nconst DISABLE_WORKER_CACHE_KEY = 'disable_worker';\n\n/** @typedef {import('./sandbox').SandboxOptions} SandboxOptions */\n/** @typedef {import('../wallet/keystore').Keystore} Keystore */\n\n/**\n * @access public\n * @typedef {object} SupervisorOptions\n * @property {Address} paymentAddress - Address to deposit earned funds into\n * @property {Keystore} identity - Keystore to use as the supervisor's identity\n * @property {string[]} [jobAddresses=[]] - If set, the supervisor will only fetch work for the provided jobIDs\n * @property {boolean} [localExec=false] - If true, fetched work will not be filtered by compute groups.\n * @property {boolean} [priorityOnly=false] - Whether to only work on priority jobs, i.e. become idle if `jobAddresses` is empty.\n * @property {SandboxOptions} [sandboxOptions] - Options that will be passed to the Sandbox constructor\n * @property {number} [watchdogInterval] - Number of ms between watchdog cycles, defaults to dcpConfig tuning param\n * @property {object[]} [computeGroups] - The compute group descriptors the worker will accept jobs from (+ optionally the default compute group)\n * @property {object} [minimumWage] - The minimum payout per slice the worker will accept from a job.\n * @property {string[]} [allowedOrigins] - origins for fetching data URIs that are allowed\n * @property {boolean} [leavePublicGroup=false] - Don't fetch slices from public compute group.\n * @property {object} [schedulerConfig] - Overrides for dcpConfig.scheduler.\n * @property {number} [maxWorkingSandboxes] - Max number of concurrently working sandboxes\n * @property {{cpu: number, gpu: number}} [cores] - The number of CPU vCores and GPU devices available for compute.\n * @property {{cpu: number, gpu: number}} [targetLoad] - The proportion of the cores.cpu and cores.gpu to load.\n */\n\nfunction disableWorker() {\n localStorage.setItem(DISABLE_WORKER_CACHE_KEY, true);\n}\n\n/**\n * Fired when the worker begins fetching slices from the scheduler.\n * @access public\n * @event Worker#fetchStart\n */\nclass Worker extends EventEmitter {\n /**\n * Returns a new Worker instance.\n * @access public\n * @param {module:dcp/worker~SupervisorOptions} supervisorOptions \n */\n constructor(supervisorOptions) {\n super('Worker');\n /**\n * @type {boolean}\n * @access public\n */\n this.working = false;\n /**\n * @type {SchedMsg}\n * @access public\n */\n this.schedMsg = new SchedMsg(this);\n /**\n * @type {Supervisor}\n * @access public\n */\n this.supervisor = new Supervisor(this, supervisorOptions);\n \n debugging() && console.debug('Worker supervisorOptions:', supervisorOptions);\n\n this.supervisor.on('fetchingTask', () => this.emit('fetchStart'));\n //this.supervisor.on('fetchedTask', (fetchedSlicesCount) => this.emit('fetch', fetchedSlicesCount)); // AFAICT UNUSED -- XXXpfr\n this.supervisor.on('fetchedTask', (fetchedSlicesCount) => this.emit('fetchEnd', fetchedSlicesCount));\n this.supervisor.on('fetchTaskFailed', (error) => this.emit('fetchEnd', error));\n this.supervisor.on('fetchTaskFailed', (error) => this.emit('fetchError', error));\n\n this.supervisor.on('submittingResults', () => this.emit('submitStart'));\n this.supervisor.on('submittedResult', () => this.emit('submitEnd'));\n this.supervisor.on('submitResultsFailed', (error) => this.emit('submitEnd', error));\n this.supervisor.on('submitResultsFailed', (error) => this.emit('submitError', error));\n this.supervisor.on('submittedResult', () => this.emit('submit'));\n \n this.supervisor.on('dccCredit', (event) => this.emit('payment', event));\n this.supervisor.on('dccNoCredit', (event) => this.emit('payment', event));\n\n this.supervisor.on('sandboxReady', (sandbox) => this.emit('sandbox', sandbox));\n \n this.supervisor.on('error', (error) => this.emit('error', error));\n this.supervisor.on('warning', (warning) => this.emit('warning', warning));\n }\n\n /**\n * Disables worker instances from being started. The user will need to manually intervene to re-enable workers.\n * \n * @access public\n */\n static disableWorker() {\n disableWorker();\n }\n\n /**\n * Starts the worker.\n * \n * @access public\n */\n async start() {\n if (this.working) throw new Error('Cannot start worker: Already working.');\n\n if (localStorage.getItem(DISABLE_WORKER_CACHE_KEY)) {\n await confirmPrompt(`Worker has been disabled by the DCP Security Team; check the @DC_Protocol Twitter feed for more information before continuing.`)\n if (await confirmPrompt('Are you sure you would like to restart the worker?')) {\n localStorage.removeItem(DISABLE_WORKER_CACHE_KEY);\n console.log(\"Starting worker...\");\n } else {\n return;\n }\n }\n\n this.working = true;\n await this.supervisor.work();\n await this.schedMsg.start();\n this.emit('start');\n }\n\n /**\n * Stops the worker.\n * \n * @access public\n * @param {boolean} [immediate=false] Whether the worker should stop imediately or allow the current slices to finish.\n */\n async stop(immediate=false) {\n if (!this.working) throw new Error('Cannot stop worker: Already stopped.');\n \n this.working = false;\n await this.schedMsg.stop();\n await this.supervisor.stopWork(immediate);\n this.emit('stop');\n }\n \n /**\n * Set payment address\n * @param {Address} addr - new address to be used\n */\n setPaymentAddress(addr)\n {\n assert(addr instanceof Address);\n this.supervisor.paymentAddress = addr;\n this.emit('paymentAddressChange', addr);\n }\n \n /**\n * Get payment address\n * @returns {Address} - current payment address.\n */\n getPaymentAddress()\n {\n return this.supervisor.paymentAddress;\n }\n \n /**\n * Set identity keystore.\n * Note: connections to the scheduler will only use the new identity if they are closed and recreated.\n * @param {Keystore} ks - new identity to be used\n */\n setIdentity(ks)\n {\n assert(ks instanceof Keystore);\n \n /* compatibility for supervisor 1 */\n if (this.supervisor.setDefaultIdentityKeystore)\n this.supervisor.setDefaultIdentityKeystore(ks);\n else\n this.supervisor.identity = ks;\n this.emit('identityChange', ks);\n }\n \n /**\n * Get identity keystore\n * @returns {Keystore} - the current identity keystore\n */\n getIdentity()\n {\n /* compatibiliy for supervisor 1 */\n if (this.supervisor._identityKeystore)\n return this.supervisor._identityKeystore;\n else\n return this.supervisor.identity;\n }\n \n /**\n * Set max working sandboxes\n * @param {number} max - new max working sandboxes\n */\n setMaxWorkingSandboxes(max)\n {\n this.supervisor.maxWorkingSandboxes = max;\n this.emit('maxSandboxesChange', max);\n }\n \n /**\n * Get max working sandboxes\n * @returns {number} - current max working sandboxes\n */\n getMaxWorkingSandboxes()\n {\n return this.supervisor.maxWorkingSandboxes;\n }\n \n /**\n * Check if there are any working sandboxes within the worker\n * @returns {Boolean} - true if there are working sandboxes.\n */\n hasWorkingSandboxes()\n {\n /* compatibility for supervisor 1 */\n if (this.supervisor.workingSandboxes)\n return this.supervisor.workingSandboxes.length > 0\n else\n return this.supervisor.workingSandboxCount() > 0;\n }\n}\n\nexports.Worker = Worker;\nexports.Supervisor = Supervisor;\nexports.disableWorker = disableWorker;\n\nexports.version = {\n api: '1.0.0',\n provides: '1.0.0' /* dcpConfig.scheduler.compatibility.operations.work */\n};\n\n\n//# sourceURL=webpack://dcp/./src/dcp-client/worker/index.js?");
|
|
4448
|
+
eval("/**\n * @file This module implements the Worker API, used to create workers for earning DCCs.\n * @author Ryan Rossiter <ryan@kingsds.network>\n * Paul <paul@kingsds.network>\n * @date May 2020\n * June, July 2022\n * \n * @module dcp/worker\n * @access public\n */\n// @ts-check\n\n\nconst debugging = (__webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope)('worker');\nconst { SchedMsg } = __webpack_require__(/*! dcp/dcp-client/schedmsg */ \"./src/dcp-client/schedmsg/index.js\");\nconst { EventEmitter } = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\");\nconst { localStorage } = __webpack_require__(/*! dcp/common/dcp-localstorage */ \"./src/common/dcp-localstorage.js\");\nconst { confirmPrompt } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\nconst { assert } = __webpack_require__(/*! dcp/common/dcp-assert */ \"./src/common/dcp-assert.js\");\nconst { Keystore, Address } = __webpack_require__(/*! dcp/dcp-client/wallet */ \"./src/dcp-client/wallet/index.js\");\n\n\n// To use Supervisor2 set the environment variable `USE_SUPERVISOR2`.\nconst USE_SUPERVISOR2 = Boolean((__webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\").getenv)('USE_SUPERVISOR2'));\nconst { Supervisor } = USE_SUPERVISOR2 ? __webpack_require__(/*! ./supervisor2 */ \"./src/dcp-client/worker/supervisor2/index.js\") : __webpack_require__(/*! ./supervisor */ \"./src/dcp-client/worker/supervisor.js\");\n\nconst DISABLE_WORKER_CACHE_KEY = 'disable_worker';\n\n/** @typedef {import('./sandbox').SandboxOptions} SandboxOptions */\n/** @typedef {import('../wallet/keystore').Keystore} Keystore */\n\n/**\n * @access public\n * @typedef {object} SupervisorOptions\n * @property {Address} paymentAddress - Address to deposit earned funds into\n * @property {Keystore} identity - Keystore to use as the supervisor's identity\n * @property {string[]} [jobAddresses=[]] - If set, the supervisor will only fetch work for the provided jobIDs\n * @property {boolean} [localExec=false] - If true, fetched work will not be filtered by compute groups.\n * @property {boolean} [priorityOnly=false] - Whether to only work on priority jobs, i.e. become idle if `jobAddresses` is empty.\n * @property {SandboxOptions} [sandboxOptions] - Options that will be passed to the Sandbox constructor\n * @property {number} [watchdogInterval] - Number of ms between watchdog cycles, defaults to dcpConfig tuning param\n * @property {object[]} [computeGroups] - The compute group descriptors the worker will accept jobs from (+ optionally the default compute group)\n * @property {object} [minimumWage] - The minimum payout per slice the worker will accept from a job.\n * @property {string[]} [allowedOrigins] - origins for fetching data URIs that are allowed\n * @property {boolean} [leavePublicGroup=false] - Don't fetch slices from public compute group.\n * @property {object} [schedulerConfig] - Overrides for dcpConfig.scheduler.\n * @property {number} [maxWorkingSandboxes] - Max number of concurrently working sandboxes\n * @property {{cpu: number, gpu: number}} [cores] - The number of CPU vCores and GPU devices available for compute.\n * @property {{cpu: number, gpu: number}} [targetLoad] - The proportion of the cores.cpu and cores.gpu to load.\n */\n\nfunction disableWorker() {\n localStorage.setItem(DISABLE_WORKER_CACHE_KEY, true);\n}\n\n/**\n * Fired when the worker begins fetching slices from the scheduler.\n * @access public\n * @event Worker#fetchStart\n */\nclass Worker extends EventEmitter {\n /**\n * Returns a new Worker instance.\n * @access public\n * @param {module:dcp/worker~SupervisorOptions} supervisorOptions \n */\n constructor(supervisorOptions) {\n super('Worker');\n /**\n * @type {boolean}\n * @access public\n */\n this.working = false;\n /**\n * @type {SchedMsg}\n * @access public\n */\n this.schedMsg = new SchedMsg(this);\n /**\n * @type {Supervisor}\n * @access public\n */\n this.supervisor = new Supervisor(this, supervisorOptions);\n \n debugging() && console.debug('Worker supervisorOptions:', supervisorOptions);\n\n this.supervisor.on('fetchingTask', () => this.emit('fetchStart'));\n //this.supervisor.on('fetchedTask', (fetchedSlicesCount) => this.emit('fetch', fetchedSlicesCount)); // AFAICT UNUSED -- XXXpfr\n this.supervisor.on('fetchedTask', (fetchedSlicesCount) => this.emit('fetchEnd', fetchedSlicesCount));\n this.supervisor.on('fetchTaskFailed', (error) => this.emit('fetchEnd', error));\n this.supervisor.on('fetchTaskFailed', (error) => this.emit('fetchError', error));\n\n this.supervisor.on('submittingResults', () => this.emit('submitStart'));\n this.supervisor.on('submittedResult', () => this.emit('submitEnd'));\n this.supervisor.on('submitResultsFailed', (error) => this.emit('submitEnd', error));\n this.supervisor.on('submitResultsFailed', (error) => this.emit('submitError', error));\n this.supervisor.on('submittedResult', () => this.emit('submit'));\n \n this.supervisor.on('dccCredit', (event) => this.emit('payment', event));\n this.supervisor.on('dccNoCredit', (event) => this.emit('payment', event));\n\n this.supervisor.on('sandboxReady', (sandbox) => this.emit('sandbox', sandbox));\n \n this.supervisor.on('error', (error) => this.emit('error', error));\n this.supervisor.on('warning', (warning) => this.emit('warning', warning));\n }\n\n /**\n * Disables worker instances from being started. The user will need to manually intervene to re-enable workers.\n * \n * @access public\n */\n static disableWorker() {\n disableWorker();\n }\n\n /**\n * Starts the worker.\n * \n * @access public\n */\n async start() {\n if (this.working) throw new Error('Cannot start worker: Already working.');\n\n if (localStorage.getItem(DISABLE_WORKER_CACHE_KEY)) {\n await confirmPrompt(`Worker has been disabled by the DCP Security Team; check the @DC_Protocol Twitter feed for more information before continuing.`)\n if (await confirmPrompt('Are you sure you would like to restart the worker?')) {\n localStorage.removeItem(DISABLE_WORKER_CACHE_KEY);\n console.log(\"Starting worker...\");\n } else {\n return;\n }\n }\n\n this.working = true;\n await this.supervisor.work();\n await this.schedMsg.start();\n this.emit('start');\n }\n\n /**\n * Stops the worker.\n * \n * @access public\n * @param {boolean} [immediate=false] Whether the worker should stop imediately or allow the current slices to finish.\n */\n async stop(immediate=false) {\n if (!this.working) throw new Error('Cannot stop worker: Already stopped.');\n \n this.working = false;\n debugging('shutdown') && console.debug(`151: stopping schedMsg...`);\n await this.schedMsg.stop();\n debugging('shutdown') && console.debug(`153: schedmsg stopped ok; stopping supervisor...`);\n await this.supervisor.stopWork(immediate);\n debugging('shutdown') && console.debug(`155: supervisor stopped ok`);\n this.emit('stop');\n }\n \n /**\n * Set payment address\n * @param {Address} addr - new address to be used\n */\n setPaymentAddress(addr)\n {\n assert(addr instanceof Address);\n this.supervisor.paymentAddress = addr;\n this.emit('paymentAddressChange', addr);\n }\n \n /**\n * Get payment address\n * @returns {Address} - current payment address.\n */\n getPaymentAddress()\n {\n return this.supervisor.paymentAddress;\n }\n \n /**\n * Set identity keystore.\n * Note: connections to the scheduler will only use the new identity if they are closed and recreated.\n * @param {Keystore} ks - new identity to be used\n */\n setIdentity(ks)\n {\n assert(ks instanceof Keystore);\n \n /* compatibility for supervisor 1 */\n if (this.supervisor.setDefaultIdentityKeystore)\n this.supervisor.setDefaultIdentityKeystore(ks);\n else\n this.supervisor.identity = ks;\n this.emit('identityChange', ks);\n }\n \n /**\n * Get identity keystore\n * @returns {Keystore} - the current identity keystore\n */\n getIdentity()\n {\n /* compatibiliy for supervisor 1 */\n if (this.supervisor._identityKeystore)\n return this.supervisor._identityKeystore;\n else\n return this.supervisor.identity;\n }\n \n /**\n * Set max working sandboxes\n * @param {number} max - new max working sandboxes\n */\n setMaxWorkingSandboxes(max)\n {\n this.supervisor.maxWorkingSandboxes = max;\n this.emit('maxSandboxesChange', max);\n }\n \n /**\n * Get max working sandboxes\n * @returns {number} - current max working sandboxes\n */\n getMaxWorkingSandboxes()\n {\n return this.supervisor.maxWorkingSandboxes;\n }\n \n /**\n * Check if there are any working sandboxes within the worker\n * @returns {Boolean} - true if there are working sandboxes.\n */\n hasWorkingSandboxes()\n {\n /* compatibility for supervisor 1 */\n if (this.supervisor.workingSandboxes)\n return this.supervisor.workingSandboxes.length > 0\n else\n return this.supervisor.workingSandboxCount() > 0;\n }\n}\n\nexports.Worker = Worker;\nexports.Supervisor = Supervisor;\nexports.disableWorker = disableWorker;\n\nexports.version = {\n api: '1.0.0',\n provides: '1.0.0' /* dcpConfig.scheduler.compatibility.operations.work */\n};\n\n\n//# sourceURL=webpack://dcp/./src/dcp-client/worker/index.js?");
|
|
4449
4449
|
|
|
4450
4450
|
/***/ }),
|
|
4451
4451
|
|
|
@@ -4583,7 +4583,7 @@ eval("/* provided dependency */ var process = __webpack_require__(/*! ./node_mod
|
|
|
4583
4583
|
\****************************************/
|
|
4584
4584
|
/***/ ((__unused_webpack_module, exports, __webpack_require__) => {
|
|
4585
4585
|
|
|
4586
|
-
eval("/**\n * @file events/event-subscriber.js\n * @author Ryan Rossiter <ryan@kingsds.network>\n * @date March 2020\n * \n * This file is the client-side companion to the event-router.\n * It maintains a map of subscription tokens that the event router has provisioned\n * for it, and calls the associated callbacks when the event router emits a new event.\n */\n\nconst protocolV4 = __webpack_require__(/*! dcp/protocol-v4 */ \"./src/protocol-v4/index.js\");\nconst { DcpURL } = __webpack_require__(/*! dcp/common/dcp-url */ \"./src/common/dcp-url.js\");\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\nconst debugging = (__webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope)('event-subscriber');\nconst { v4: uuidv4 } = __webpack_require__(/*! uuid */ \"./node_modules/uuid/dist/esm-browser/index.js\");\nconst { leafMerge } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\nconst dcpEnv = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\n\n/** @typedef {import('./common-types').DcpEvent} DcpEvent */\n/** @typedef {import('./common-types').SubscriptionToken} SubscriptionToken */\n/** @typedef {import('./common-types').EventLabel} EventLabel */\n/** @typedef {import('./common-types').SubscriptionOptions} SubscriptionOptions */\n\n\n/**\n * A container for callback and SubscriptionToken pairs\n * @typedef {Object} CallbackHandle\n * @property {SubscriptionToken} token\n * @property {function} callback\n */\n\n/**\n * @typedef {Object} Subscription\n * @property {EventLabel} label - Event label\n * @property {function[]} callbacks - The callbacks that will be invoked when an event is received for this subscription\n * @property {SubscriptionOptions} options - Additional options\n * @property {string} optionsStr - A string used to memoize stringifying the options object\n */\n\n/**\n * @constructor\n * @param {object} options configuration options for this instance of the EventSubscriber. Default\n * options are given via dcpConfig.eventSubscriber.\n */\nclass EventSubscriber\n{\n constructor(eventEmitter, options)\n {\n this.options = options = leafMerge(options, dcpConfig.eventSubscriber);\n this.eventEmitter = eventEmitter;\n this.subscriptions = new Map();\n this.seenEventIds = [];\n this.nextSeenEventIdSlot = 0;\n \n /**\n * Interval to keep the connection alive when no messages are being\n * received.\n * @type {NodeJS.Timer}\n */\n this._keepaliveIntervalHnd = null;\n\n this.onEventRouterConnectionInterrupted = () => {\n this.openEventRouterConn();\n this.setupEventRouterConnectionEvents();\n this.reestablishSubscriptions();\n }\n\n const ceci = this;\n\n this.eventRouterConnection = null;\n this.openEventRouterConn = function openEventRouterConn()\n {\n ceci.eventRouterConnection = new protocolV4.Connection(dcpConfig.scheduler.services.eventRouter);\n ceci.eventRouterConnection.on('close', ceci.onEventRouterConnectionInterrupted);\n }\n this.openEventRouterConn();\n this.setupEventRouterConnectionEvents();\n }\n\n async close() {\n debugging() && console.log(`event-subscriber: closing EventSubscriber connection`);\n\n let subs = this.subscriptions.entries();\n const ps = [];\n for (let [label] of subs) {\n ps.push(this.unsubscribe(label));\n }\n await Promise.all(ps);\n\n this.eventRouterConnection.off('close', this.onEventRouterConnectionInterrupted);\n\n this.setKeepalive(false);\n await this.eventRouterConnection.close()\n .finally(() => {\n this.subscriptions.clear();\n this.hookedEventRouterConnectionEvents = false;\n });\n }\n\n async reestablishSubscriptions() {\n let subs = this.subscriptions.entries();\n debugging() && console.log(`event-subscriber: reestablishing ${this.subscriptions.size} subscriptions`);\n\n const eventTypes = (__webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\").eventTypes);\n const reliableEvents = [];\n const optionalEvents = [];\n for (let [label, oldToken] of subs) {\n if (oldToken.options)\n {\n // schedmsg special case\n await this.provisionSubscriptionToken(label, oldToken.options);\n }\n else if (eventTypes[label] && eventTypes[label].reliable)\n {\n reliableEvents.push(label);\n }\n else\n {\n optionalEvents.push(label);\n }\n }\n try\n {\n if (reliableEvents.length || optionalEvents.length) {\n const message = {\n reliableEvents,\n optionalEvents,\n options: this.options\n }\n\n await this.provisionSubscriptionTokens(message);\n }\n \n }\n catch (error)\n {\n debugging() && console.warn(`event-subscriber: error establishing subscription (${sub.label}):`, error);\n }\n }\n\n setupEventRouterConnectionEvents() {\n debugging() && console.log(`event-subscriber: setupEventRouterConnectionEvents`)\n this.eventRouterConnection.on('request', (req) => {\n const { operation } = req.payload;\n if (operation === 'event') this.onEventRequest(req);\n else req.respond(new Error(`Unknown EventSubscriber operation: ${operation}`));\n });\n }\n\n async onEventRequest(req) {\n debugging('verbose') && console.log(`event-subscriber: onEventRequest`);\n // data = req.payload.data.event\n const { token, event: data } = req.payload.data;\n const event = data.data;\n const eventId = data.eventId;\n\n debugging() && console.debug(`131: event ${eventId} (${event.eventName}):`, req.payload.data);\n if (this.subscriptions.has(event.eventName))\n {\n const { token: eventToken } = this.subscriptions.get(event.eventName);\n debugging('verbose') && console.log(`event-subscriber: event: ${event.eventName}`);\n if (eventToken !== token)\n req.respond(new Error(`No subscription registered for token ${token}`));\n else if (eventId && this.seenEventIds.includes(eventId))\n req.respond(new Error(`This event has already been sent: ${eventId}`));\n else\n {\n const label = event.eventName;\n // on each request, we push the eventIds to keep track of events we have seen.\n // this will be filtered in the re-established events.\n if (eventId)\n {\n this.seenEventIds[this.nextSeenEventIdSlot] = eventId;\n this.nextSeenEventIdSlot = (this.nextSeenEventIdSlot + 1) % Number(this.options.seenListSize);\n }\n \n delete event.eventName;\n if (!this.eventEmitter.eventIntercepts)\n this.eventEmitter.emit(label, event);\n else\n {\n if (this.eventEmitter.eventIntercepts[label])\n this.eventEmitter.eventIntercepts[label](event)\n else\n this.eventEmitter.emit(label, event);\n }\n req.respond();\n }\n }\n else\n {\n console.warn(`No subscription registered for label ${event.eventName}`);\n req.respond(new Error(`No subscription registered for token ${event.eventName}`));\n }\n }\n\n /**\n * Registers this subscription with the event router, and saves the returned\n * subscription token into the this.subscriptions map.\n * @param {EventLabel} label\n * @param {SubscriptionOptions} options\n */\n async provisionSubscriptionToken(label, options) {\n debugging('verbose') && console.log(`event-subscriber: provisionSubscriptionToken(${label})`);\n if (this.eventRouterConnection.state.in(['closed','closing','close-wait']))\n this.openEventRouterConn();\n let res = await this.eventRouterConnection.send('subscribe', {\n label, options,\n }).catch(async (error) => {\n debugging('verbose') && console.error(`Failed to send subscription request to scheduler for the event ${label}: ${error}. Will try again.`);\n this.eventRouterConnection.close();\n });\n \n if (res) {\n const token = res.payload.token;\n this.subscriptions.set(label, { token, options });\n }\n\n }\n\n async provisionSubscriptionTokens(message) {\n debugging('verbose') && console.log(`event-subscriber: provisionSubscriptionTokens(${JSON.stringify(message)})`);\n let res = await this.eventRouterConnection.send('subscribeMany', message).catch(async (e) => {\n debugging('verbose') && console.error(`Failed to send subscribeMany request to scheduler: ${e}. Will try again`);\n this.eventRouterConnection.close();\n });\n \n if (res && res.success) {\n const tokenEventPairs = res.payload.tokens;\n tokenEventPairs.forEach((tokenAndEvent) => {\n const label = tokenAndEvent.event;\n const token = tokenAndEvent.token;\n this.subscriptions.set(label, { token });\n })\n } else if (res && !res.success) {\n throw new Error(`Failed to subscribe to events, ${res.payload}`);\n }\n \n }\n\n /**\n * Unregisters a subscription from the event router\n * @param {EventLabel} label\n * @param {SubscriptionToken} token \n */\n async releaseSubscriptionToken(label, token) {\n debugging('verbose') && console.log(`event-subscriber: releaseSubscriptionToken(${label})`);\n await this.eventRouterConnection.send('unsubscribe', {\n label, token,\n }).catch(async (e) => {\n debugging('verbose') && console.error(`Failed to unsubscribe from event ${label}: ${e}. Will try again.`)\n this.eventRouterConnection.close();\n });\n }\n\n /**\n * @param {EventLabel} label - Event label\n * @param {function} callback\n * @param {SubscriptionOptions} options - Additional options\n */\n async subscribeManyEvents(reliableEvents, optionalEvents, options={}) {\n this.options = options;\n const noSubReliableEvents = [];\n reliableEvents.forEach((ev) => {\n for (let [t, sub] of this.subscriptions)\n {\n // search for a subscription with identical label,\n // to avoid making duplicate subscriptions\n if (ev === sub.label)\n {\n return;\n }\n }\n if (!noSubReliableEvents.includes(ev)) {\n noSubReliableEvents.push(ev);\n }\n })\n\n const noSubOptionalEvents = [];\n optionalEvents.forEach((ev) => {\n for (let [t, sub] of this.subscriptions)\n {\n // search for a subscription with identical label,\n // to avoid making duplicate subscriptions\n if (ev === sub.label)\n {\n return;\n }\n }\n if (!noSubOptionalEvents.includes(ev)) {\n noSubOptionalEvents.push(ev);\n }\n })\n\n if (reliableEvents.length || optionalEvents.length)\n {\n const message = {\n reliableEvents: noSubReliableEvents,\n optionalEvents: noSubOptionalEvents,\n options,\n }\n debugging() && console.log('event-subscriber: provisioning:', message);\n await this.provisionSubscriptionTokens(message);\n }\n \n // Start the keepalive interval\n this.setKeepalive();\n }\n \n async subscribe(label, options={}) {\n debugging() && console.log(`event-subscriber: subscribe(${label})`);\n let token;\n for (let [subLabel, t] of this.subscriptions) {\n // search for a subscription with identical label and filter,\n // to avoid making duplicate subscriptions\n if (label === subLabel) {\n token = t;\n break;\n }\n }\n\n if (!token) {\n await this.provisionSubscriptionToken(label, options);\n }\n\n // Start the keepalive interval\n this.setKeepalive();\n }\n\n /**\n * @param {string} label\n */\n async unsubscribe(label) {\n debugging() && console.log(`event-subscriber: unsubscribe)${label})`);\n if (!this.subscriptions.has(label)) {\n return;\n }\n \n const token = this.subscriptions.get(label)\n await this.releaseSubscriptionToken(label, token);\n this.subscriptions.delete(label);\n\n // If no subscriptions remain, disable the keepalive and allow the Connection\n // to expire\n if (this.subscriptions.size === 0)\n this.setKeepalive(false);\n }\n\n /**\n * De/activate an interval to send keepalives over the event-router connection\n *\n * @param {Boolean} keepalive If true, activate the interval. If false, remove it\n */\n setKeepalive(activate = true)\n {\n const that = this;\n const keepaliveInterval = Number(this.options.keepaliveInterval) || 3 * 60; /* seconds */\n\n if (!activate /* => deactivate */)\n {\n /* clear the watchdog interval, and neuter already-queued doKeepalive()s */\n if (this._keepaliveIntervalHnd)\n clearInterval(this._keepaliveIntervalHnd);\n this._keepaliveIntervalHnd = null;\n return;\n }\n\n if (activate && !keepaliveInterval)\n {\n debugging() && console.debug('event-subscriber: configured for no keepalive interval');\n return;\n }\n\n if (!this._keepaliveIntervalHnd)\n this._keepaliveIntervalHnd = setInterval(doKeepalive, keepaliveInterval * 1000)\n \n if (dcpEnv.platform === 'nodejs')\n this._keepaliveIntervalHnd.unref();\n\n async function doKeepalive()\n {\n if (that._keepaliveIntervalHnd)\n {\n try\n {\n that.eventRouterConnection.keepalive();\n }\n catch(error)\n {\n /* ignore errors if the watchdog was cancelled while were waiting for the\n * keepalive to complete; this implies the connection was purposefully closed.\n */\n if (!that._keepaliveIntervalHnd)\n return;\n\n debugging() && console.debug('Event subscriber watchdog detected connection in error state', error.code || error.message);\n debugging('verbose') && console.debug(error);\n\n that.eventRouterConnection.close(); /* trigger ER connection recycle */\n }\n }\n }\n }\n}\n\nexports.EventSubscriber = EventSubscriber;\n\n\n//# sourceURL=webpack://dcp/./src/events/event-subscriber.js?");
|
|
4586
|
+
eval("/**\n * @file events/event-subscriber.js\n * @author Ryan Rossiter <ryan@kingsds.network>\n * @date March 2020\n * \n * This file is the client-side companion to the event-router.\n * It maintains a map of subscription tokens that the event router has provisioned\n * for it, and calls the associated callbacks when the event router emits a new event.\n */\n\nconst protocolV4 = __webpack_require__(/*! dcp/protocol-v4 */ \"./src/protocol-v4/index.js\");\nconst { DcpURL } = __webpack_require__(/*! dcp/common/dcp-url */ \"./src/common/dcp-url.js\");\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\nconst debugging = (__webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope)('event-subscriber');\nconst { v4: uuidv4 } = __webpack_require__(/*! uuid */ \"./node_modules/uuid/dist/esm-browser/index.js\");\nconst { leafMerge } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\nconst dcpEnv = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\n\n/** @typedef {import('./common-types').DcpEvent} DcpEvent */\n/** @typedef {import('./common-types').SubscriptionToken} SubscriptionToken */\n/** @typedef {import('./common-types').EventLabel} EventLabel */\n/** @typedef {import('./common-types').SubscriptionOptions} SubscriptionOptions */\n\n\n/**\n * A container for callback and SubscriptionToken pairs\n * @typedef {Object} CallbackHandle\n * @property {SubscriptionToken} token\n * @property {function} callback\n */\n\n/**\n * @typedef {Object} Subscription\n * @property {EventLabel} label - Event label\n * @property {function[]} callbacks - The callbacks that will be invoked when an event is received for this subscription\n * @property {SubscriptionOptions} options - Additional options\n * @property {string} optionsStr - A string used to memoize stringifying the options object\n */\n\n/**\n * @constructor\n * @param {object} options configuration options for this instance of the EventSubscriber. Default\n * options are given via dcpConfig.eventSubscriber.\n */\nclass EventSubscriber\n{\n constructor(eventEmitter, options)\n {\n this.options = options = leafMerge(options, dcpConfig.eventSubscriber);\n this.eventEmitter = eventEmitter;\n this.subscriptions = new Map();\n this.seenEventIds = [];\n this.nextSeenEventIdSlot = 0;\n \n /**\n * Interval to keep the connection alive when no messages are being\n * received.\n * @type {NodeJS.Timer}\n */\n this._keepaliveIntervalHnd = null;\n\n this.onEventRouterConnectionInterrupted = () => {\n this.openEventRouterConn();\n this.setupEventRouterConnectionEvents();\n this.reestablishSubscriptions();\n }\n\n const ceci = this;\n\n this.eventRouterConnection = null;\n this.openEventRouterConn = function openEventRouterConn()\n {\n ceci.eventRouterConnection = new protocolV4.Connection(dcpConfig.scheduler.services.eventRouter);\n ceci.eventRouterConnection.on('close', ceci.onEventRouterConnectionInterrupted);\n }\n this.openEventRouterConn();\n this.setupEventRouterConnectionEvents();\n }\n\n async close() {\n debugging() && console.log(`event-subscriber: closing EventSubscriber connection`);\n\n let subs = this.subscriptions.entries();\n const ps = [];\n for (let [label] of subs) {\n ps.push(this.unsubscribe(label));\n }\n await Promise.all(ps);\n\n this.eventRouterConnection.off('close', this.onEventRouterConnectionInterrupted);\n\n this.setKeepalive(false);\n\n debugging() && console.log(`event-subscriber: closing event-router connection...`);\n this.eventRouterConnection.close('schedmsg closing');\n this.subscriptions.clear();\n this.hookedEventRouterConnectionEvents = false;\n }\n\n async reestablishSubscriptions() {\n let subs = this.subscriptions.entries();\n debugging() && console.log(`event-subscriber: reestablishing ${this.subscriptions.size} subscriptions`);\n\n const eventTypes = (__webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\").eventTypes);\n const reliableEvents = [];\n const optionalEvents = [];\n for (let [label, oldToken] of subs) {\n if (oldToken.options)\n {\n // schedmsg special case\n await this.provisionSubscriptionToken(label, oldToken.options);\n }\n else if (eventTypes[label] && eventTypes[label].reliable)\n {\n reliableEvents.push(label);\n }\n else\n {\n optionalEvents.push(label);\n }\n }\n try\n {\n if (reliableEvents.length || optionalEvents.length) {\n const message = {\n reliableEvents,\n optionalEvents,\n options: this.options\n }\n\n await this.provisionSubscriptionTokens(message);\n }\n \n }\n catch (error)\n {\n debugging() && console.warn(`event-subscriber: error establishing subscription (${sub.label}):`, error);\n }\n }\n\n setupEventRouterConnectionEvents() {\n debugging() && console.log(`event-subscriber: setupEventRouterConnectionEvents`)\n this.eventRouterConnection.on('request', (req) => {\n const { operation } = req.payload;\n if (operation === 'event') this.onEventRequest(req);\n else req.respond(new Error(`Unknown EventSubscriber operation: ${operation}`));\n });\n }\n\n async onEventRequest(req) {\n debugging('verbose') && console.log(`event-subscriber: onEventRequest`);\n // data = req.payload.data.event\n const { token, event: data } = req.payload.data;\n const event = data.data;\n const eventId = data.eventId;\n\n debugging() && console.debug(`131: event ${eventId} (${event.eventName}):`, req.payload.data);\n if (this.subscriptions.has(event.eventName))\n {\n const { token: eventToken } = this.subscriptions.get(event.eventName);\n debugging('verbose') && console.log(`event-subscriber: event: ${event.eventName}`);\n if (eventToken !== token)\n req.respond(new Error(`No subscription registered for token ${token}`));\n else if (eventId && this.seenEventIds.includes(eventId))\n req.respond(new Error(`This event has already been sent: ${eventId}`));\n else\n {\n const label = event.eventName;\n // on each request, we push the eventIds to keep track of events we have seen.\n // this will be filtered in the re-established events.\n if (eventId)\n {\n this.seenEventIds[this.nextSeenEventIdSlot] = eventId;\n this.nextSeenEventIdSlot = (this.nextSeenEventIdSlot + 1) % Number(this.options.seenListSize);\n }\n \n delete event.eventName;\n if (!this.eventEmitter.eventIntercepts)\n this.eventEmitter.emit(label, event);\n else\n {\n if (this.eventEmitter.eventIntercepts[label])\n this.eventEmitter.eventIntercepts[label](event)\n else\n this.eventEmitter.emit(label, event);\n }\n req.respond();\n }\n }\n else\n {\n console.warn(`No subscription registered for label ${event.eventName}`);\n req.respond(new Error(`No subscription registered for token ${event.eventName}`));\n }\n }\n\n /**\n * Registers this subscription with the event router, and saves the returned\n * subscription token into the this.subscriptions map.\n * @param {EventLabel} label\n * @param {SubscriptionOptions} options\n */\n async provisionSubscriptionToken(label, options) {\n debugging('verbose') && console.log(`event-subscriber: provisionSubscriptionToken(${label})`);\n\n if (this.eventRouterConnection.state.in(['closed','closing','close-wait']))\n {\n this.openEventRouterConn();\n this.setupEventRouterConnectionEvents();\n }\n let res = await this.eventRouterConnection.send('subscribe', {\n label, options,\n }).catch(async (error) => {\n debugging('verbose') && console.error(`Failed to send subscription request to scheduler for the event ${label}: ${error}. Will try again.`);\n this.eventRouterConnection.close();\n });\n \n if (res) {\n const token = res.payload.token;\n this.subscriptions.set(label, { token, options });\n }\n\n }\n\n async provisionSubscriptionTokens(message) {\n debugging('verbose') && console.log(`event-subscriber: provisionSubscriptionTokens(${JSON.stringify(message)})`);\n let res = await this.eventRouterConnection.send('subscribeMany', message).catch(async (e) => {\n debugging('verbose') && console.error(`Failed to send subscribeMany request to scheduler: ${e}. Will try again`);\n this.eventRouterConnection.close();\n });\n \n if (res && res.success) {\n const tokenEventPairs = res.payload.tokens;\n tokenEventPairs.forEach((tokenAndEvent) => {\n const label = tokenAndEvent.event;\n const token = tokenAndEvent.token;\n this.subscriptions.set(label, { token });\n })\n } else if (res && !res.success) {\n throw new Error(`Failed to subscribe to events, ${res.payload}`);\n }\n \n }\n\n /**\n * Unregisters a subscription from the event router\n * @param {EventLabel} label\n * @param {SubscriptionToken} token \n */\n async releaseSubscriptionToken(label, token) {\n debugging('verbose') && console.log(`event-subscriber: releaseSubscriptionToken(${label})`);\n await this.eventRouterConnection.send('unsubscribe', {\n label, token,\n }).catch(async (e) => {\n debugging('verbose') && console.error(`Failed to unsubscribe from event ${label}: ${e}. Will try again.`)\n this.eventRouterConnection.close();\n });\n }\n\n /**\n * @param {EventLabel} label - Event label\n * @param {function} callback\n * @param {SubscriptionOptions} options - Additional options\n */\n async subscribeManyEvents(reliableEvents, optionalEvents, options={}) {\n this.options = options;\n const noSubReliableEvents = [];\n reliableEvents.forEach((ev) => {\n for (let [t, sub] of this.subscriptions)\n {\n // search for a subscription with identical label,\n // to avoid making duplicate subscriptions\n if (ev === sub.label)\n {\n return;\n }\n }\n if (!noSubReliableEvents.includes(ev)) {\n noSubReliableEvents.push(ev);\n }\n })\n\n const noSubOptionalEvents = [];\n optionalEvents.forEach((ev) => {\n for (let [t, sub] of this.subscriptions)\n {\n // search for a subscription with identical label,\n // to avoid making duplicate subscriptions\n if (ev === sub.label)\n {\n return;\n }\n }\n if (!noSubOptionalEvents.includes(ev)) {\n noSubOptionalEvents.push(ev);\n }\n })\n\n if (reliableEvents.length || optionalEvents.length)\n {\n const message = {\n reliableEvents: noSubReliableEvents,\n optionalEvents: noSubOptionalEvents,\n options,\n }\n debugging() && console.log('event-subscriber: provisioning:', message);\n await this.provisionSubscriptionTokens(message);\n }\n \n // Start the keepalive interval\n this.setKeepalive();\n }\n \n async subscribe(label, options={}) {\n debugging() && console.log(`event-subscriber: subscribe(${label})`);\n let token;\n for (let [subLabel, t] of this.subscriptions) {\n // search for a subscription with identical label and filter,\n // to avoid making duplicate subscriptions\n if (label === subLabel) {\n token = t;\n break;\n }\n }\n\n if (!token) {\n await this.provisionSubscriptionToken(label, options);\n }\n\n // Start the keepalive interval\n this.setKeepalive();\n }\n\n /**\n * @param {string} label\n */\n async unsubscribe(label) {\n debugging() && console.log(`event-subscriber: unsubscribe)${label})`);\n if (!this.subscriptions.has(label)) {\n return;\n }\n \n const token = this.subscriptions.get(label)\n await this.releaseSubscriptionToken(label, token);\n this.subscriptions.delete(label);\n\n // If no subscriptions remain, disable the keepalive and allow the Connection\n // to expire\n if (this.subscriptions.size === 0)\n this.setKeepalive(false);\n }\n\n /**\n * De/activate an interval to send keepalives over the event-router connection\n *\n * @param {Boolean} keepalive If true, activate the interval. If false, remove it\n */\n setKeepalive(activate = true)\n {\n const that = this;\n const keepaliveInterval = Number(this.options.keepaliveInterval) || 3 * 60; /* seconds */\n\n if (!activate /* => deactivate */)\n {\n /* clear the watchdog interval, and neuter already-queued doKeepalive()s */\n if (this._keepaliveIntervalHnd)\n clearInterval(this._keepaliveIntervalHnd);\n this._keepaliveIntervalHnd = null;\n return;\n }\n\n if (activate && !keepaliveInterval)\n {\n debugging() && console.debug('event-subscriber: configured for no keepalive interval');\n return;\n }\n\n if (!this._keepaliveIntervalHnd)\n this._keepaliveIntervalHnd = setInterval(doKeepalive, keepaliveInterval * 1000)\n \n if (dcpEnv.platform === 'nodejs')\n this._keepaliveIntervalHnd.unref();\n\n async function doKeepalive()\n {\n if (that._keepaliveIntervalHnd)\n {\n try\n {\n that.eventRouterConnection.keepalive();\n }\n catch(error)\n {\n /* ignore errors if the watchdog was cancelled while were waiting for the\n * keepalive to complete; this implies the connection was purposefully closed.\n */\n if (!that._keepaliveIntervalHnd)\n return;\n\n debugging() && console.debug('Event subscriber watchdog detected connection in error state', error.code || error.message);\n debugging('verbose') && console.debug(error);\n\n that.eventRouterConnection.close(); /* trigger ER connection recycle */\n }\n }\n }\n }\n}\n\nexports.EventSubscriber = EventSubscriber;\n\n\n//# sourceURL=webpack://dcp/./src/events/event-subscriber.js?");
|
|
4587
4587
|
|
|
4588
4588
|
/***/ }),
|
|
4589
4589
|
|
|
@@ -4647,7 +4647,7 @@ eval("/**\n * @file protocol/connection/message.js\n * @author Ryan
|
|
|
4647
4647
|
/***/ ((__unused_webpack_module, exports, __webpack_require__) => {
|
|
4648
4648
|
|
|
4649
4649
|
"use strict";
|
|
4650
|
-
eval("/* provided dependency */ var process = __webpack_require__(/*! ./node_modules/process/browser.js */ \"./node_modules/process/browser.js\");\n/**\n * @file protocol/connection/connection.js\n * @author Ryan Rossiter\n * @author KC Erb\n * @author Wes Garland\n * @date January 2020, Feb 2021, Mar 2022\n *\n * A Connection object represents a connection to another DCP entity. \n * A DCP connection may 'live' longer than the underlying protocol's connection,\n * and the underlying protocol connection (or, indeed, protocol) may change\n * throughout the life of the DCP connection.\n * \n * DCP connections are uniquely identified by the DCP Session ID, specified by\n * the dcpsid property, present in every message body. This session id negotiated during connection,\n * with the initiator and target each providing half of the string.\n *\n * Connection instance events:\n * - session: dcpsid new session established\n * - connect: url UI hint: internet available\n * - disconnect: url UI hint: internet not available\n * - readyStateChange: *** DO NOT USE **\n * - error: error emitted when errors happen that would otherwise go uncaught\n * - close: connection instance is closing\n * - end: Connection instance is closed\n * - send: msgObj when a message is sent to the peer; does not wait for ack; may re-trigger on reconnect\n * - ready: when the connection is ready for traffic (constructor promises resolved)\n *\n * State Transition Diagram for Connection.state:\n *\n * initial connecting established disconnected close-wait closing closed\n * ===========================================================================================================================\n * |-- i:connect ---->\n * |-- t:accept ----->\n * |-- t:establishTarget -->\n * |-- i:connect ---------->\n * |-- transportDisconnectHandler -->\n * <-- i:_reconnect ----------------------------------------|\n * |-i:useNewTransport-->\n * <-- t:useNewTransport --------|\n * |-- closeWait ----------------------------------------------------------->\n * |-- closeWait ----------------------------------->\n * |-- closeWait -->\n * |-- doClose --------------->\n * |-- close ------------------------------------------------------------------------------------------------------------> \n * |-- close ---------------------------------------------------------------------------->\n * |-- close ---------------------------------------------------->\n * |-- close ------------------->\n * |-- doClose -->\n *\n *\n * Not until the established state can we count on things like a dcpsid, \n * peerAddress, identityPromise resolution and so on.\n * \n * Error Codes relevant to DCP Connections:\n * DCPC-1001 - CONNECTION CANNOT SEND WHEN IN CLOSING, CLOSE-WAIT OR CLOSED\n * EINVAL - MESSAGE OWNER IS INVALID (formerly DCPC-1002)\n * MESSAGE SIGNATURE INVALID (formerly DCPC-1003)\n * MESSAGE BODY IS INVALID (formerly DCPC-1004)\n * DCPC-1005 - TRYING TO ESTABLISH TARGET AFTER TARGET ALREADY ESTABLISHED\n * DCPC-1006 - CONNECTION COULD NOT BE ESTABLISHED WITHIN 30 SECONDS\n * DCPC-1007 - RECEIVED MESSAGE PAYLOAD BEFORE CONNECT OPERATION\n * DCPC-1008 - TARGET RESPONDED WITH INVALID DCPSID\n * DCPC-1009 - MESSAGE IS OF UNKNOWN TYPE\n * DCPC-1010 - DUPLICATE TRANSMISSION RECEIPT\n * DCPC-1011 - DEFAULT ERROR CODE WHEN PEER SENDS CLOSE MESSAGE\n * DCPC-1012 - TRIED TO INITIATE CONNECTION AFTER SESSION ALREADY ESTABLISHED\n * DCPC-1013 - DEFAULT ERROR CODE WHEN CLOSING WITH REASON THATS NOT INSTANCE OF DCPERROR\n * DCPC-1014 - NO TRANSPORTS AVAILABLE\n * DCPC-1015 - CANNOT CONNECT WHEN CONNECTION ALREADY CLOSED\n * DCPC-1016 - ERROR CONNECTING VIA AVAILABLE TRANSPORTS\n * DCPC-1017 - FIRST PROTOCOL MESSAGE WAS DID NOT INVOLVE INITIAL CONNECT REQUEST\n * DCPC-1018 - INVALID ARGUMENT PROVIDED IN PLACE OF IDKEYSTORE\n * ENODCPSID - CONNECTION INSTANCE TRIED TO RE-CONNECT TO A TARGET WHICH DOES NOT HAVE A RECORD OF THAT SESSION\n */\n\n\n\nconst debugging = (__webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope)('dcp');\nconst dcpEnv = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\nconst { EventEmitter } = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\");\nconst wallet = __webpack_require__(/*! dcp/dcp-client/wallet */ \"./src/dcp-client/wallet/index.js\");\nconst { DcpURL } = __webpack_require__(/*! dcp/common/dcp-url */ \"./src/common/dcp-url.js\");\nconst { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\nconst { assert } = __webpack_require__(/*! dcp/common/dcp-assert */ \"./src/common/dcp-assert.js\");\nconst { leafMerge, a$sleepMs } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\nconst { Synchronizer } = __webpack_require__(/*! dcp/common/concurrency */ \"./src/common/concurrency.js\");\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\n\nconst { Transport } = __webpack_require__(/*! ../transport */ \"./src/protocol-v4/transport/index.js\");\nconst { Sender } = __webpack_require__(/*! ./sender */ \"./src/protocol-v4/connection/sender.js\");\nconst { Receiver } = __webpack_require__(/*! ./receiver */ \"./src/protocol-v4/connection/receiver.js\");\nconst { MessageLedger } = __webpack_require__(/*! ./message-ledger */ \"./src/protocol-v4/connection/message-ledger.js\");\nconst { getGlobalIdentityCache } = __webpack_require__(/*! ./identity-cache */ \"./src/protocol-v4/connection/identity-cache.js\");\nconst { makeEBOIterator, setImmediateN, setImmediate } = __webpack_require__(/*! dcp/common/dcp-timers */ \"./src/common/dcp-timers.js\");\n\nconst { ConnectionMessage } = __webpack_require__(/*! ./connection-message */ \"./src/protocol-v4/connection/connection-message.js\");\nconst { ConnectionRequest } = __webpack_require__(/*! ./request */ \"./src/protocol-v4/connection/request.js\");\nconst { ConnectionResponse } = __webpack_require__(/*! ./response */ \"./src/protocol-v4/connection/response.js\");\nconst { ConnectionBatch } = __webpack_require__(/*! ./batch */ \"./src/protocol-v4/connection/batch.js\");\nconst { ConnectionAck } = __webpack_require__(/*! ./ack */ \"./src/protocol-v4/connection/ack.js\");\nconst { ErrorPayloadCtorFactory } = __webpack_require__(/*! ./error-payload */ \"./src/protocol-v4/connection/error-payload.js\");\nconst { role } = __webpack_require__(/*! ./connection-constants */ \"./src/protocol-v4/connection/connection-constants.js\");\n\nconst isDebugBuild = (__webpack_require__(/*! dcp/common/dcp-build */ \"./src/common/dcp-build.js\").build) === 'debug';\nlet nanoid;\nif (dcpEnv.platform === 'nodejs') {\n const { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\n nanoid = requireNative('nanoid').nanoid;\n} else {\n nanoid = (__webpack_require__(/*! nanoid */ \"./node_modules/nanoid/index.browser.js\").nanoid);\n}\n\n\nlet globalConnectionId = 0;\nvar _msgId = 0;\n\nconst CONNECTION_STATES = [\n 'initial',\n 'connecting', /* initiator: establish first transport instance connection; target: listening */\n 'established',\n 'disconnected', /* connection is still valid, but underlying transport is no longer connected */\n 'close-wait', /* Target of close message is in this state until response is acknowledged */\n 'closing',\n 'closed',\n]\n\nclass Connection extends EventEmitter {\n static get VERSION() {\n return '5.1.0'; // Semver format\n }\n\n static get VERSION_COMPATIBILITY() {\n return '^5.0.0'; // Semver format, can be a range\n }\n\n /**\n * Connection form 2:\n * @constructor\n * @param {object} [target]\n * @param {Promise} idKsPromise A promise which resolves to the identity keystore described\n * in form 1\n * @param {object} [options]\n * @see form 1\n */\n /**\n * Connection form 1\n * Create a DCP Connection object. This object could represent either the initiator or \n * target end of a connection, until it is specialized by either invoke the connect()\n * or accept() methods. Note that send() invokes connect() internally if not in an established\n * state.\n * @constructor\n * @note Connection objects exist for the lifetime of a given DCP connection \n * (session), whether or not the underlying transport (eg internet protocol) is connected or not. Once \n * the DCP session has ended, this object has no purpose and is not reusable.\n * \n * @param {object} target Object (usually a dcpConfig fragment) describing the target.\n * This object may contain the following properties; 'location' is\n * mandatory:\n * - location: a URL or DcpURL that is valid from the Internet\n * - friendLocation: a DcpURL that is valid from an intranet; if\n * both location and friendLocation specified, the best one will\n * be chosen by examining IP addresses (assuming an IP bearer).\n * - identity: an object with an address property which is a promise\n * that resolves to an instance of wallet.Address which represents\n * to the target's identity; this overrides the initiator's \n * identity cache unless options.strict is truey.\n * \n * @param {Keystore} [idKeystore] The keystore used to sign messages; used for non-repudiation.\n * If not specified, a dynamically-generated keystore will be used.\n * \n * @param {object} [options] Extra connection options that aren't defined via dcpConfig.dcp.connectionOptions.\n * These options include:\n * - identityUnlockTimeout: Number of (floating-point) seconds to leave the identity \n * keystore unlocked between invocations of Connection.send\n *\n * @returns instance of Connection that is specific to a target URL but not a role\n */\n constructor(target, idKeystore, options = {})\n {\n super('Connection');\n this.id = ++globalConnectionId;\n this.debugLabel = `connection(g:${this.id}):`;\n\n /* polymorphism strategy: rewrite to (configFragment, idksPromise, options) */\n if (target instanceof DcpURL)\n target = { location: target };\n else if (DcpURL.isURL(target))\n target = { location: new DcpURL(target) };\n else if (target instanceof String || typeof target === 'string')\n target = { location: new DcpURL(target) };\n assert(typeof target === 'object', target.location);\n\n /* idKeystore is always resolved by the time a session is established. */\n if (!idKeystore)\n this.identityPromise = wallet.getId();\n else if (idKeystore instanceof Promise)\n this.identityPromise = idKeystore;\n else if (idKeystore instanceof wallet.Keystore)\n this.identityPromise = Promise.resolve(idKeystore); \n else if (idKeystore instanceof wallet.Address)\n this.identityPromise = Promise.resolve(new wallet.Keystore(idKeystore, '')); \n else\n throw new DCPError('Invalid argument provided for IdKeystore', 'DCPC-1018');\n\n this.identityPromise.then((keystore) => {\n /* This always happens by the time a role is assumed */\n delete this.identityPromise;\n this.identity = keystore;\n this.emit('ready');\n debugging('connection') && console.debug(this.debugLabel, 'identity is', this.identity.address);\n });\n\n this.target = target;\n this.url = this.target.location;\n \n // Init internal state / vars\n this.state = new Synchronizer(CONNECTION_STATES[0], CONNECTION_STATES);\n // DO NOT USE this.state.on('change', (s) => this.emit('readyStateChange', s) );\n\n this.dcpsid = null;\n this.peerAddress = null;\n this.transport = null;\n this.messageLedger = new MessageLedger(this);\n this.authorizedSender = null;\n this.preDisconnectState = null;\n \n this.Message = ConnectionMessage(this);\n this.Request = ConnectionRequest(this.Message);\n this.Response = ConnectionResponse(this.Message);\n this.Batch = ConnectionBatch(this.Message);\n this.Ack = ConnectionAck(this.Message);\n this.ErrorPayload = ErrorPayloadCtorFactory(this);\n this.connectTime = Date.now();\n\n this.receiver = new Receiver(this);\n this.sender = new Sender(this);\n \n debugging('connection') && console.debug(this.debugLabel, `new connection#${this.id}; ${this.url}`);\n\n /* Create a connection config as this.options which takes into\n * account system defaults and overrides for specific urls, origins, etc.\n */\n this.options = leafMerge(\n ({ /* hardcoded defaults insulate us from missing web config */\n 'connectTimeout': 90,\n 'lingerTimeout': 1800,\n 'allowBatch': true,\n 'maxMessagesPerBatch': 100,\n 'identityUnlockTimeout': 300,\n 'ttl': {\n 'min': 15,\n 'max': 600,\n 'default': 120\n },\n 'transports': [ 'socketio' ],\n }),\n dcpConfig.dcp.connectionOptions.default,\n this.url && dcpConfig.dcp.connectionOptions[this.url.hostname],\n this.url && dcpConfig.dcp.connectionOptions[this.url.origin],\n options\n );\n\n /* draw out errors quickly in dev */\n if ((process.env.DCP_NETWORK_CONFIG_BUILD || dcpConfig.build) === 'debug')\n {\n this.options.maxMessagesPerBatch /= 10;\n \n /* short timeouts and debuggers don't get along well */\n if (dcpEnv.platform === 'nodejs' && !(requireNative('module')._cache.niim instanceof requireNative('module').Module))\n {\n this.options.connectTimeout /= 10;\n this.options.lingerTimeout /= 20;\n this.options.identityUnlockTimeout /= 10;\n }\n }\n\n assert(this.options.identityUnlockTimeout > 0);\n assert(this.options.connectTimeout > 0);\n assert(this.options.lingerTimeout > 0);\n assert(typeof this.options.ttl.min === 'number');\n assert(typeof this.options.ttl.max === 'number');\n assert(typeof this.options.ttl.default === 'number');\n \n this.backoffTimeIterator = makeEBOIterator(500, dcpConfig.build === 'debug' ? 5000 : 60000); /** XXXwg make this configurable */\n\n this.secureLocation = determineIfSecureLocation(this);\n this.loggableDest = '<generic>';\n }\n \n generateMessageId()\n {\n return `${this.id}-${_msgId++}-${Date.now()}-${nanoid()}`;\n }\n\n /**\n * Specialize an instance of Connection for either initiator or target behaviour. Once specialized,\n * the role cannot be changed. This happens based on methods invoked; connect() or accept() cause\n * the change.\n *\n * This specialization also implies that the connection is fully ready for use, including resolution\n * of the identity promise if necessary. This is perhaps not the best place to ensure that, but it\n * provides a reliable - and already async - waypoint to observe that event.\n */\n async a$assumeRole(myRole)\n {\n assert(myRole === role.initiator || myRole === role.target);\n\n if (this.role === myRole)\n return;\n this.role = myRole;\n \n if (this.role === role.target)\n {\n this.debugLabel = `connection(t:${this.id}):`;\n this.sender.debugLabel = `sender(t#${this.id}):`;\n this.messageLedger.debugLabel = `message-ledger(t#${this.id}):`;\n this.loggableDest = '<target>';\n this.hasNtp = true;\n }\n else\n {\n this.debugLabel = `connection(i:${this.id}):`;\n this.sender.debugLabel = `sender(i#${this.id}):`;\n this.messageLedger.debugLabel = `message-ledger(i#${this.id}):`;\n this.loggableDest = this.url.href;\n this.hasNtp = false;\n }\n\n debugging('connection') && console.debug(this.debugLabel, `connection #${this.id} is ${this.role} for ${this.url}`);\n if (!this.identity)\n {\n assert(this.identityPromise);\n debugging('connection') && console.debug(this.debugLabel, `waiting for identity resolution`);\n await this.identityPromise;\n }\n }\n\n /**\n * API to establish a DCP connection. Implied by send().\n *\n * When invoked by the initiator, this method establishes the connection by connecting\n * to the target url provided to the constructor.\n */\n async connect() // eslint-disable-line require-await\n {\n if (this.role == role.target)\n return;\n \n if (!this.role)\n await this.a$assumeRole(role.initiator);\n \n if (this.state.is('initial'))\n {\n if (!this.connectPromise)\n this.connectPromise = Promise.race([this.connectTimer(), this.a$_connect()]).then(() => { clearTimeout(this.connectTimeoutPromise); delete this.connectPromise });\n return this.connectPromise;\n }\n\n if (this.state.is('disconnected'))\n {\n if (!this.connectPromise)\n this.connectPromise = this.a$_reconnect().then(() => delete this.connectPromise);\n return this.connectPromise;\n }\n \n if (this.state.is('connecting'))\n {\n assert(this.connectPromise);\n return this.connectPromise;\n }\n\n if (this.state.is('established'))\n return;\n \n if (this.state.in(['closed', 'close-wait', 'closing']))\n throw new DCPError('Connection already closed', 'DCPC-1015');\n\n throw new Error('impossible');\n }\n\n /**\n * Performs a reconnection for connections which are in the disconnected state, and\n * tries to send any in-flight or enqueued messages as soon as that happens.\n */\n async a$_reconnect()\n {\n var transport;\n\n this.state.testAndSet('disconnected', 'connecting');\n try\n {\n do\n {\n transport = await this.a$connectToTargetTransport();\n } while (!transport && (this.transport && !this.transport.ready()) && !this.state.in(['closed', 'close-wait', 'closing']));\n\n debugging('connection') && console.debug(this.debugLabel, `reconnected via transport ${transport.socket.id}`);\n \n this.useNewTransport(transport);\n }\n catch (error)\n {\n if (error.code !== 'DCPC-1016' && error.code !== 'DCPC-1015')\n {\n /* Unreached unless there are bugs. */\n throw error;\n } \n this.close(error, true);\n }\n }\n\n connectTimer()\n {\n return new Promise((_resolve, reject) =>\n {\n var timeoutMs = (this.options.connectTimeout2 || 31) * 1000;\n \n this.connectTimeoutPromise = setTimeout(() =>\n { \n reject(new Error(`Failed to establish connection to ${this.url} within ${(timeoutMs / 1000).toFixed(1)} seconds`));\n }, timeoutMs);\n if (typeof this.connectTimeoutPromise.unref === 'function')\n this.connectTimeoutPromise.unref();\n });\n }\n \n async a$_connect()\n { \n var presharedPeerAddress, establishResults;\n var targetIdentity = await this.target.identity;\n var transport;\n \n assert(this.role === role.initiator);\n\n this.state.set('initial', 'connecting');\n do\n {\n transport = await this.a$connectToTargetTransport().catch((error) =>\n {\n debugging('connection') && console.debug(`${this.debugLabel} error connecting to target on transport layer:`, error);\n return { ready: () => {return false} };\n });\n } while(!transport.ready());\n this.adopt(transport);\n\n establishResults = await this.sender.establish().catch(error => {\n debugging('connection') && console.debug(this.debugLabel, `Could not establish DCP session ${this.transport ? 'over' + this.transport.name : '. Transport establishment was not complete'}:`, error);\n this.close(error, true);\n throw error;\n });\n const peerAddress = new wallet.Address(establishResults.peerAddress);\n const dcpsid = establishResults.dcpsid;\n debugging('connection') && console.debug(this.debugLabel, 'dcpsid is', dcpsid);\n \n if (!this.options.strict && targetIdentity && determineIfSecureConfig())\n {\n if ( false\n || typeof targetIdentity !== 'object'\n || typeof targetIdentity.address !== 'object'\n || !(targetIdentity.address instanceof wallet.Address))\n targetIdentity = { address: new wallet.Address(targetIdentity) }; /* map strings and Addresses to ks ducks */\n\n presharedPeerAddress = targetIdentity.address;\n debugging('connection') && console.debug(this.debugLabel, 'Using preshared peer address', presharedPeerAddress);\n }\n this.ensureIdentity(peerAddress, presharedPeerAddress);\n\n /* At this point, new session is valid & security checks out - make Connection instance usable */\n this.peerAddress = peerAddress;\n if (this.dcpsid)\n throw new DCPError(`Reached impossible state in connection.js; dcpsid already specified ${this.dcpsid} (${this.url})`, 'DCPC-1012');\n\n this.state.set('connecting', 'established'); /* established => dcpsid has been set */\n this.emit('session', (this.dcpsid = dcpsid));\n this.emit('connect', this.url);\n this.sender.notifyTransportReady();\n return Promise.resolve();\n }\n\n /**\n * unreference any objects entrained by this connection so that it does not prevent\n * the node program from exiting naturally.\n */\n unref()\n {\n if (this.connectAbortTimer && this.connectAbortTimer.unref && dcpEnv.platform === 'nodejs')\n this.connectAbortTimer.unref();\n }\n\n /**\n * Method is invoked when the transport disconnects. Transport instance is responsible for its own\n * finalization; Connection instance is responsible for finding a new transport, resuming the\n * connection, and retransmitting any in-flight message.\n *\n * @param {object} transport the transport instance that triggered this handler. In some cases, it\n * is possible that this event is not serviced until after the connection\n * has already acquired a new transport instance, e.g. in a Target where\n * the initiator switched networks. This implies that it is possible for\n * more 'connect' events to be emitted than 'disconnect' events.\n */\n transportDisconnectHandler(transport)\n {\n try\n { \n if (this.state.in(['disconnected', 'closing', 'close-wait', 'closed'])) /* transports may fire this more than once */\n return;\n\n if (transport !== this.transport) /* event no longer relevant */\n return;\n\n if (this.transport)\n {\n transport.close();\n delete this.transport;\n }\n \n if (this.state.is('established'))\n {\n this.state.set('established', 'disconnected');\n this.emit('disconnect', this.url); /* UI hint: \"internet unavailable\" */\n debugging('connection') && console.debug(this.debugLabel, `Transport disconnected from ${this.url}; ${this.sender.inFlight ? 'have' : 'no'} in-flight message`);\n \n if (!this.dcpsid) /* hopefully impossible? */\n {\n debugging('connection') && console.debug(this.debugLabel, 'Not reconnecting - no session');\n return;\n }\n }\n \n if (this.role === role.target)\n {\n /* targets generally can't reconnect due to NAT */\n debugging('connection') && console.debug(this.debugLabel, `Waiting for initiator to reconnect for ${this.dcpsid}`);\n return;\n }\n \n if (this.dcpsid && !this.sender.inFlight && this.options.onDemand)\n {\n debugging('connection') && console.debug(this.debugLabel, `Not reconnecting ${this.dcpsid} until next message`);\n return;\n }\n \n if (this.state.is('connecting') && (!this.dcpsid || !this.peerAddress))\n {\n debugging('connection') && console.debug(this.debugLabel, `Disconnected while connecting, establishing transport and re-sending connect request.`);\n this.a$_reconnect();\n return;\n }\n\n /* At this point, we initiate a (re)connect attempt because either\n * - we haven't connected yet,\n * - we have something to send, or\n * - we are not an on-demand connection\n */\n if (!this.state.is('connecting'))\n this.connect();\n }\n catch(error)\n {\n debugging('connection') && console.debug(error);\n this.close(error, true);\n \n if (error.code !== 'DCPC-1016' && error.code !== 'DCPC-1015')\n {\n /* Unreached unless there are bugs. */\n throw error;\n }\n }\n }\n \n /**\n * Initiators only\n *\n * Connect to a target at the transport level.\n * - Rejects when we give up on all transports.\n * - Resolves with a transport instance when we connect to one.\n *\n * The connection attempt will keep a node program \"alive\" while it is happening.\n * The `autoUnref` connectionOption and unref() methods offer ways to make this not\n * happen.\n */\n async a$connectToTargetTransport()\n {\n const that = this;\n const availableTransports = [].concat(this.options.transports);\n var quitMsg = false; /* not falsey => reject asap, value is error message */\n var quitCode = undefined;\n var boSleepIntr; /* if not falsey, a function that interrupts the backoff sleep */\n var transportConnectIntr; /* if not falsey, a function that interrupts the current connection attempt */\n\n // Already trying to connect to target, don't try multiple times until we've aborted one attempt\n if (this.connectAbortTimer)\n return;\n \n /* This timer has the lifetime of the entire connection attempt. When we time out,\n * we set the quitMsg to get the retry loop to quit, then we interrupt the timer so\n * that we don't have to wait for the current backoff to expire before we notice, and\n * we expire the current attempt to connect right away as well.\n */\n this.connectAbortTimer = setTimeout(() => {\n quitMsg = 'connection timeout';\n if (boSleepIntr) boSleepIntr();\n if (transportConnectIntr) transportConnectIntr();\n }, this.options.connectTimeout * 1000);\n\n if (this.options.autoUnref)\n this.unref();\n\n /* cleanup code called on return/throw */\n function cleanup_ctt()\n {\n clearTimeout(that.connectAbortTimer);\n delete that.connectAbortTimer;\n }\n\n /* Connect to target with a specific transport. */\n /* Resolves with { bool success, obj transport } or rejects with { error } if the transport cannot connect*/\n function a$connectWithTransport(transportName)\n { \n transportConnectIntr = false;\n\n return new Promise((connectWithTransport_resolve, connectWithTransport_reject) => { \n const TransportClass = Transport.require(transportName);\n const transport = new TransportClass(that.target, that.options[transportName]);\n var ret = { transport };\n\n function cleanup_cwt()\n {\n for (let eventName of transport.eventNames())\n for (let listener of transport.listeners(eventName))\n transport.off(eventName, listener);\n }\n \n /* In the case where we have a race condition in the transport implementation, arrange things\n * so that we resolve with whatever fired last if we have a double-fire on the same pass of \n * the event loop.\n */\n transport.on('connect', () => { cleanup_cwt(); ret.success=true; connectWithTransport_resolve(ret) });\n transport.on('error', (error) => { cleanup_cwt(); connectWithTransport_reject(error) });\n transport.on('connect-failed', (error) => {\n cleanup_cwt();\n ret.success = false;\n ret.error = error;\n debugging() && console.debug(`Error connecting to ${that.url};`, error);\n connectWithTransport_resolve(ret);\n });\n \n /* let the connectAbortTimer interrupt this connect attempt */\n transportConnectIntr = () => { transport.close() };\n });\n }\n \n if (availableTransports.length === 0)\n {\n cleanup_ctt();\n return Promise.reject(new DCPError('no transports defined', 'DCPC-1014'));\n }\n \n /* Loop while trying each available transport in turn. Sleep with exponential backoff between runs */\n while (!quitMsg)\n {\n for (let transportName of availableTransports)\n {\n try\n {\n const { success, error, transport } = await a$connectWithTransport(transportName);\n\n /* Have connected to the remote at the transport level - OUT */\n if (success === true)\n {\n transportConnectIntr = false;\n cleanup_ctt();\n \n return transport;\n }\n\n /* Fast-fail: certain - but few - HTTP status codes let us know that this (or any) transport\n * will never work, so don't try those again.\n */\n if (error && error.httpStatus)\n {\n switch(error.httpStatus)\n {\n case 301: case 302: case 303: case 307: case 308:\n debugging('connection') && console.debug(this.debugLabel, `HTTP status ${error.httpStatus}; won't try again with ${transportName}`);\n availableTransports.splice(availableTransports.indexOf(transportName), 1);\n break;\n case 400: case 403: case 404:\n debugging('connection') && console.debug(this.debugLabel, `HTTP status ${error.httpStatus}; won't try again.`);\n quitMsg = error.message;\n quitCode = 'HTTP_' + error.httpStatus || 0;\n break;\n default:\n debugging('connection') && console.debug(this.debugLabel, `HTTP status ${error.httpStatus}; will try again with ${transportName}`);\n break;\n }\n }\n }\n catch (impossibleError)\n {\n /* transport connection attempts should never throw. */\n debugging('connection') && console.debug(this.debugLabel, `Error connecting to ${this.url} with ${transportName}; won't try again:`, impossibleError);\n availableTransports.splice(availableTransports.indexOf(transportName), 1);\n }\n }\n \n if (availableTransports.length === 0)\n {\n quitMsg = 'all transports exhausted';\n break;\n }\n \n /* Go to (interruptible) sleep for a while before trying again */\n const backoffTimeMs = this.backoffTimeIterator.next().value;\n debugging('connection') && console.debug(this.debugLabel, 'trying again in', Number(backoffTimeMs / 1000).toFixed(2), 'seconds');\n const boSleepPromise = a$sleepMs(backoffTimeMs);\n boSleepIntr = boSleepPromise.intr;\n await boSleepPromise;\n boSleepIntr = false;\n }\n\n /* The only way we get here is for us to discover that the connection is unconnectable - eg \n * reject timer has expired or similar.\n */\n cleanup_ctt();\n throw new DCPError(quitMsg, 'DCPC-1016', quitCode);\n }\n\n /**\n * Method which must be invoked whenever a new transport needs to be assigned to the connection. \n * If we have previously adopted a transport, we close it first, which will prevent \n * [by transport definition] any message handlers from firing, even if the old transport instance\n * has buffered traffic.\n *\n * @param {object} transport transport instance\n */\n adopt(transport)\n {\n if (this.transport)\n this.transport.close();\n\n transport.on('message', (m) => this.handleMessage(m));\n transport.on('end', () => this.transportDisconnectHandler(transport));\n transport.on('close', () => this.transportDisconnectHandler(transport));\n \n this.transport = transport;\n }\n \n \n /**\n * Method that gets invoked when there is a new transport available for adoption.\n * This will adjust the state of the connection, adopt the transport then tell\n * the sender to pump the message queue.\n * @param {object} transport transport instance \n */\n useNewTransport(transport)\n {\n if (this.state.in(['closing', 'close-wait', 'closed']))\n {\n debugging('connection') && console.debug(`${this.debugLabel} got a new transport during closing. closing the new transport and not completing transport adoption.`)\n transport.close();\n return;\n }\n \n var preDisconnectState = (!this.dcpsid || !this.peerAddress) ? 'connecting' : 'established';\n if (this.state.isNot(preDisconnectState))\n this.state.set(['connecting', 'disconnected'], preDisconnectState);\n this.adopt(transport);\n this.emit('connect', this.url); // UI hint: \"internet available\" \n this.sender.notifyTransportReady();\n }\n \n /**\n * Method that must be invoked by the target to \"accept\" a new DCP Connection request\n * at the transport layer.\n * @param {object} transport \n */\n async accept(transport)\n {\n assert(!this.role);\n await this.a$assumeRole(role.target);\n this.state.set('initial', 'connecting');\n this.adopt(transport);\n }\n \n /**\n * This method is invoked by the target when it has handled the initial connect request from\n * the initiator, which contains the peerAddress and the first half of the dcpsid (the second half is\n * populated by receiver.handleFirstRequest before being passed here). It transitions the connection \n * into an established state at the protocol level.\n * Note - this is really not the right design for this, but it is invoked from handleFirstRequest\n * in ./receiver.js\n *\n * @param {string} dcpsid dcpsid\n * @param {wallet.Address} peerAddress Address of peer\n */\n establishTarget(dcpsid, peerAddress) {\n assert(this.role === role.target);\n \n this.connectResponseId = Symbol('dummy'); // un-register ConnectResponse\n this.peerAddress = peerAddress;\n if (this.dcpsid)\n throw new DCPError(`Reached impossible state in connection.js; dcpsid already specified ${this.dcpsid}!=${dcpsid} (${this.url})`, 'DCPC-1005');\n this.emit('session', (this.dcpsid = dcpsid));\n debugging() && console.debug(this.debugLabel, 'dcpsid is', dcpsid);\n\n this.loggableDest = this.role === role.initiator ? this.url : peerAddress;\n this.state.set('connecting', 'established'); /* established => dcpsid has been set */\n\n debugging('connection') && console.debug(this.debugLabel, `Established session ${this.dcpsid} with ${this.peerAddress} for ${this.url}`);\n }\n\n /**\n * Check to see if the peer address conflicts with what we have in the global identity cache;\n * it does, throw an exception.\n */\n ensureIdentity (peerAddress, presharedPeerAddress)\n {\n let idc = getGlobalIdentityCache();\n let noConflict = idc.learnIdentity(this.url, peerAddress, presharedPeerAddress);\n\n if (!noConflict)\n throw new DCPError(`**** Security Error: Identity address ${peerAddress} does not match the saved key for ${this.url}`, 'DCPC-EADDRCHANGE');\n }\n \n \n /**\n * This method uses the first request (if we're target) or ack to the first request \n * (if we're initiator) to memoize the address of the peer authorized to send to us. \n * The first message must only be a request (since it comes from sender.specialFirstSend)\n * that has a connect operation, or an acknowledgement of the first request. \n * All future messages' owners will be validated against this authorized sender.\n * @param {Object} message \n */\n setAuthorizedSender(message)\n {\n if (message.body.type !== 'request' && message.body.type !== 'ack')\n throw new DCPError('First protocol message was not a request or ack', 'DCPC-1017');\n \n if (message.body.type === 'request' && message.body.payload.operation !== 'connect')\n throw new DCPError('First protocol message did not contain the correct payload', 'DCPC-1017');\n \n if (message.body.type === 'ack' && this.sender.inFlight.message.payload.operation !== 'connect')\n throw new DCPError('First protocol acknowledgement was not for connect request', 'DCPC-1017');\n \n this.authorizedSender = message.owner;\n }\n \n /**\n * Emits an error event with the relevant error and closes the connection immediately.\n * @param {string} errorMessage \n * @param {string} errorCode \n */\n \n handleValidationError(errorMessage, errorCode)\n {\n var messageError;\n \n debugging('connection') && console.debug(this.debugLabel, 'Message failed validation -', errorMessage);\n this.emit('error', (messageError = new DCPError(`message failed validation: ${errorMessage}`, errorCode)))\n this.close(messageError, true);\n }\n\n /**\n * This method validates the message owner, signature and body before passing it onto\n * either the receiver (for a request, response or batch) or the messageLedger (for an ack).\n * If it's a request, response or batch, this method also provokes the connection to \n * send an acknowledgement (ack) to the peer to let them know we got their message.\n * XXXwg this code needs an audit re error handling: what message error should we be emitting?\n * why do we keep working after we find an error?\n * XXXsc did some auditing. we happy now?\n * @param {string} JSON-encoded unvalidated message object\n */\n async handleMessage (messageJSON) {\n var validation;\n var message;\n\n if (this.state.is('closed')) {\n debugging('connection') && console.debug(this.debugLabel, 'handleMessage was called on a closed connection.');\n return;\n }\n\n try\n {\n message = typeof messageJSON === 'object' ? messageJSON : JSON.parse(messageJSON);\n debugging('wire') && console.debug(this.debugLabel, `handleMessage: ${String(message && message.body && message.body.type).padEnd(10, ' ')} <- ${this.loggableDest}`);\n }\n catch(error)\n {\n console.error('connection::handleMessage received unparseable message from peer:', error);\n this.emit('error', error);\n return;\n }\n \n /**\n * We always ack a duplicate transmission.\n * This must happen before validation since during startup we may lack a\n * nonce or dcpsid (depending on whether initiator or target + race).\n */\n if (this.isDuplicateTransmission(message)) {\n debugging('connection') && console.debug(this.debugLabel, 'duplicate message:', message.body);\n debugging('wire') && console.debug(this.debugLabel, `dup message ack: ${String(message.body.type).padEnd(10, ' ')} -> ${this.loggableDest}`);\n\n this.sendAck(this.lastAckSigned) \n return;\n }\n\n debugging('connection') && console.debug(this.debugLabel, `received message ${message.body.type} ${message.body.id}; nonce=`, message.body.nonce);\n \n validation = this.validateMessageDCPSID(message);\n if (validation.success !== true)\n {\n this.handleValidationError(validation.errorMessage, 'ENODCPSID');\n return;\n }\n\n validation = this.validateMessageOwner(message)\n if (validation.success !== true)\n {\n this.handleValidationError(validation.errorMessage, 'EINVAL');\n return;\n }\n \n validation = this.validateMessageSignature(message);\n if (validation.success !== true)\n {\n this.handleValidationError(validation.errorMessage, 'EINVAL');\n return;\n }\n\n validation = this.validateMessageBody(message);\n if (validation.success !== true)\n {\n this.handleValidationError(validation.errorMessage, validation.errorCode || 'EINVAL'); /* messages of type 'unhandled-message' may contain more information about the failure */\n return;\n }\n \n if (message.body.type === \"ack\") {\n const ack = new this.Ack(message.body);\n this.messageLedger.handleAck(ack);\n return;\n } else if (message.body.type !== 'unhandled-message') {\n this.lastMessage = message;\n await this.ackMessage(message);\n }\n \n this.receiver.handleMessage(message);\n }\n\n \n /**\n * This method takes either a Request, Response or Batch, creates an ack for it\n * and sends it to the peer. This ack contains the nonce we expect on the next\n * message from peer.\n * @param {Connection.Message} message \n */\n async ackMessage(message) {\n debugging('connection') && console.debug(this.debugLabel, 'acking message of type: ', message.body.type);\n const ack = new this.Ack(message);\n const signedMessage = await ack.sign(this.identity);\n\n debugging('wire') && console.debug(this.debugLabel, `ackMessage: ${String(message.body.type).padEnd(10, ' ')} -> ${this.loggableDest}`);\n\n this.sendAck(signedMessage);\n this.lastAck = ack;\n this.lastAckSigned = signedMessage;\n }\n\n /**\n * Checks if the batch we just received has the same nonce\n * as the most-recently received batch.\n * @param {object} messageJSON\n */\n isDuplicateTransmission(messageJSON) {\n return this.lastMessage && this.lastMessage.body.nonce && this.lastMessage.body.nonce === messageJSON.body.nonce;\n }\n\n /**\n * Validate that the message came from the appropriate sender.\n * @param {Object} message the message to validate\n * @returns {Object} returns an object `ret` with either ret.success = true, \n * or ret.success = false accompanied by another property ret.errorMessage\n */\n validateMessageOwner(message)\n {\n if (!this.authorizedSender)\n {\n /* Capture the initial identity of the remote end during the connect operation */\n this.setAuthorizedSender(message);\n return { success: true }\n }\n else if (message.owner !== this.authorizedSender)\n {\n return { success: false, errorMessage: \"message came from unauthorized sender\" }\n }\n return { success: true }\n }\n \n /**\n * Validate that the signature was generated from this message body\n * @param {Object} message\n * @returns {Object} with properties 'success' and 'errorMessage'. When the message is valid on its \n * face, the success property is true, otherwise it is is false. When it is false,\n * the errorMessage property will be a string explaining why.\n */\n validateMessageSignature(message)\n {\n if (!message.signature) {\n debugging('connection') && console.warn(\"Message does not have signature, aborting connection\");\n return { success: false, errorMessage: \"message is missing signature\" };\n }\n \n const owner = new wallet.Address(message.owner);\n const signatureValid = owner.verifySignature(message.body, message.signature);\n\n if (!signatureValid)\n {\n debugging('connection') && console.warn(\"Message has an invalid signature, aborting connection\");\n return { success: false, errorMessage: \"invalid message signature\" };\n }\n\n return { success: true };\n }\n \n validateMessageDCPSID(message)\n {\n if (this.dcpsid !== null && message.dcpsid)\n {\n if (message.dcpsid !== this.dcpsid)\n {\n debugging('connection') && console.warn('Message has an invalid dcpsid, aborting connection');\n return { success: false, errorMessage: 'message has an invalid dcpsid' };\n }\n }\n \n return { success: true };\n }\n /**\n * This method is used to perform validation on all types of messages.\n * It validates the DCPSID, nonce, and the peerAddress.\n * @param {Object} message\n * @returns {Object} with properties 'success' and 'errorMessage'. When the message is valid on its \n * face, the success property is true, otherwise it is is false. When it is false,\n * the errorMessage property will be a string explaining why.\n *\n */\n validateMessageBody(message)\n {\n try\n {\n if (message.body.type === 'unhandled-message')\n {\n /* This special message type may not have a dcpsid, peerAddress, etc., so it might not\n * validate. It is also not a \"real\" message and only used to report ConnectionManager routing \n * errors, so we just report here, drop it, and close the connection.\n *\n * Note also that this is probably the wrong way to handle this case - restarting daemons - but\n * that is a problem for another day. /wg nov 2021\n */\n debugging('connection') && console.warn(this.debugLabel, \"Target Error - target could not process message.\", JSON.stringify(message.body),\n \"Aborting connection.\");\n return { success: false, errorMessage: `target could not process message (${message.body.payload && message.body.payload.message || 'unknown error'})`, errorCode: message.body.payload && message.body.payload.code}\n }\n if (this.peerAddress && !this.peerAddress.eq(message.owner))\n {\n debugging('connection') && console.warn(this.debugLabel,\n \"Received message's owner address does not match peer address, aborting connection\\n\",\n \"(owner addr)\", message.owner, '\\n',\n \"(peer addr)\", this.peerAddress);\n return { success: false, errorMessage: \"received message owner does not match peer address\" };\n }\n\n if (this.state.in(['established', 'closing', 'close-wait']) && message.body.type !== 'unhandled-message')\n {\n const body = message.body;\n\n assert(this.peerAddress); /* should be set in connect */\n /**\n * Security note:\n * We don't require the dcpsid to match on an ack because the connect response\n * ack doesn't have a dcpsid until after it is processed. Also ack's are protected\n * by ack tokens and signatures, so this doesn't leave a hole, just an inconsistency.\n */\n if (body.type !== 'ack' && body.dcpsid !== this.dcpsid)\n {\n debugging('connection') && console.warn(this.debugLabel,\n \"Received message's DCPSID does not match, aborting connection\\n\",\n \"Message owner:\", message.owner, '\\n',\n \"(ours)\", this.dcpsid, (Date.now() - this.connectTime)/1000, \"seconds after connecting - state:\", this.state._, \"\\n\", \n \"(theirs)\", body.dcpsid);\n if(body.dcpsid.substring(0, body.dcpsid.length/2) !== this.dcpsid.substring(0, this.dcpsid.length/2)){\n debugging('connection') && console.warn(\" Left half of both DCPSID is different\");\n }\n if(body.dcpsid.substring(body.dcpsid.length/2 + 1, body.dcpsid.length) !== this.dcpsid.substring(this.dcpsid.length/2 + 1, body.dcpsid.length)){\n debugging('connection') && console.warn(\" Right half of both DCPSID is different\");\n }\n return { success: false, errorMessage: \"DCPSID do not match\" };\n }\n /* can get close in middle of connecting, which will have no nonce.*/\n if (body.type !== 'ack' && this.lastAck.nonce !== body.nonce && (body.payload && body.payload.operation !== 'close'))\n {\n /* When Target sends back ConnectionLessErrorResponse, it uses the nonce of the message that caused an error. */\n if (this.sender.inFlight && this.sender.inFlight.message.nonce === body.nonce)\n {\n debugging('connection') && console.debug(`${this.debugLabel} Received messages nonce matches nonce of our current inFlight message.`,\n \"There was a problem sending this message. Aborting connection. Reason:\\n\", body.payload);\n return { success: false, errorMessage: \"current inflight message returned an error\" }\n }\n debugging('connection') && console.warn(this.debugLabel,\"Received message's nonce does not match expected nonce, aborting connection\\n\");\n debugging('connection') && console.debug(this.debugLabel, this.lastAck.nonce, body.nonce);\n return { success: false, errorMessage: \"received message's nonce does not match expected nonce\" };\n }\n if (body.type === 'request') \n {\n if (body.payload.validity.time === undefined)\n return { success: false, errorMessage: 'received message does not have a valid time in its payload' };\n }\n }\n\n return { success: true };\n }\n catch(error)\n {\n console.error('message validator failure:', error);\n return { success: false, errorMessage: 'validator exception ' + error.message };\n }\n\n return { success: false, errorMessage: 'impossible code reached' }; // eslint-disable-line no-unreachable\n }\n\n /**\n * Targets Only.\n * The receiver creates a special connect response and the connection\n * needs to know about it to get ready for the ack. See `isWaitingForAck`.\n * @param {Message} message message we are sending out and waiting to\n * ack'd, probably a batch containing the response.\n */\n registerConnectResponse(message) {\n this.connectResponseId = message.id;\n }\n\n /**\n * Targets only\n * During the connection process a target sends a connect\n * response to an initiator and the initiator will ack it. Since transports\n * are not tightly coupled, we have no authoritative way to route the ack back\n * to the right connection. So a connection briefly registers the ack it\n * is looking for in this case. It will formally validate the ack after routing.\n * @param {string} messageId id of the message this ack is acknowledging.\n */\n isWaitingForAck(messageId) {\n return messageId === this.connectResponseId;\n }\n\n /**\n * Put connection into close-wait state so that a call to `close`\n * in this state will *not* trigger sending a `close` message to the peer.\n * Then call close.\n *\n * @note: This function is called when the remote end of the transport sends\n * a close command, from receiver::handleOperation. This impllies that\n * that we must be in established or later state.\n */\n closeWait (errorCode = null)\n {\n var preCloseState, reason;\n \n debugging('connection') && console.debug(this.debugLabel, `responding to close. state=closeWait dcpsid=${this.dcpsid}`);\n\n if (this.state.is('closed'))\n {\n debugging('connection') && console.debug(this.debugLabel, `remote asked us to close a closed connection; dcpsid=${this.dcpsid}`);\n return;\n }\n\n // continue with close in either case\n reason = `Received close from peer with Error Code ${errorCode}`;\n if (this.role === role.target)\n reason += ` (${this.url})`;\n else\n reason += ` (${this.debugLabel}${this.peerAddress.address})`;\n\n reason = new DCPError(reason, errorCode || 'DCPC-1011');\n\n // If we're already closing, wait for it to complete then resolve\n // WARNING: any place we transition to closing or close-wait, we MUST guarantedd\n // that 'end' will be emitted, or this code will hang forever!\n if (this.state.in(['close-wait', 'closing'])) {\n return new Promise((resolve) => {\n this.once('end', resolve) /* eventually fired by doClose elsewhere */\n });\n }\n\n /* XXXwg - this should only be established->close-wait. Why more? */\n this.state.set(['disconnected', 'connecting', 'established'], 'close-wait');\n \n /* Set preCloseState to close-wait so doClose doesn't send a close message back */\n preCloseState = this.state.valueOf();\n return this.doClose(preCloseState, reason, true);\n }\n\n /**\n * This method will begin closing the protocol connection. It transitions\n * the protocol into the correct state, and then begins the work of closing.\n * \n * @param {string|Error} [reason] Either an Error or a message to use in the Error that will reject pending sends.\n * @param {boolean} [immediate] When true, the connection will not deliver any pending messages and instead\n * immediately send the peer a 'close' request. \n *\n * @return a Promise which resolves when the connection has been confirmed closed and the end event has been fired.\n */\n close (reason='requested', immediate=false)\n {\n if (this.state.is('initial'))\n {\n this.state.set('initial', 'closed');\n this.emit('close'); /* Don't emit dcpsid */\n }\n if (this.state.is('closed')) return Promise.resolve();\n\n const preCloseState = this.state.valueOf();\n debugging('connection') && \n console.debug(this.debugLabel, \n `close; dcpsid=${this.dcpsid} state=${preCloseState} immediate=${immediate} reason:`, reason);\n\n // If we're already closing, wait for it to complete then resolve\n if (this.state.in(['close-wait', 'closing'])) {\n return new Promise((resolve) => {\n this.once('end', resolve)\n });\n }\n\n this.state.set(['connecting', 'established', 'disconnected'], 'closing');\n\n // Perform actual work of closing\n return this.doClose(preCloseState, reason, immediate);\n }\n\n /**\n * Sends close message to peer after sending all pending messages.\n * Note that close messages are sent without the expectation of a response.\n * @param {DCPError|string} reason reason for closing\n */\n async sendCloseGracefully(reason) \n {\n debugging('connection') && console.debug(`${this.debugLabel} gracefully sending close message to peer with reason ${reason}`)\n let errorCode = reason instanceof DCPError ? reason.code : 'DCPC-1011';\n \n /* This only resolves when close is next message in queue */\n const closeMessage = await this.prepare('close', { errorCode: errorCode });\n this.sendPreparedMessage(closeMessage);\n this.messageLedger.fulfillMessagePromise(closeMessage.message.id, {});\n }\n \n /**\n * Sends close message to peer immediately. Pending messages will not be sent.\n * Note that close messages are sent without expectation of response.\n * @param {DCPError|string} reason reason for closing\n */\n async sendCloseImmediately(reason)\n {\n debugging('connection') && console.debug(`${this.debugLabel} immediately sending close message to peer with reason ${reason}`);\n let errorCode = reason instanceof DCPError ? reason.code : 'DCPC-1011';\n \n /* Last param being `true` means that prepareMessage will return unsigned message. Does not queue message. */\n const closeMessage = await this.prepare('close', { errorCode: errorCode }, true);\n \n if (this.sender.inFlight)\n closeMessage.nonce = this.sender.inFlight.message.nonce;\n else\n closeMessage.nonce = this.sender.nonce;\n \n let signedCloseMessage = await closeMessage.sign();\n \n /* Overwrite the in-flight message because we don't care to deliver pending messages */\n this.sender.inFlight = { message: closeMessage, signedMessage: signedCloseMessage };\n this.sender.sendInFlightMessage();\n }\n \n /**\n * This method performs the core close functionality. It appropriately sends the close message\n * to the peer, fails any pending transmissions, shuts down our sender and underlying transport\n * and puts us into the 'closed' state, indicating this connection object is now useless.\n * When called from closeWait, it does not send a close message.\n * @param {string} preCloseState the state that the connection was in at the start of the\n * invocation of close() or closeWait()\n *\n * @note: this function is not reentrant due to closeGracefully\n */\n async doClose(preCloseState, reason, immediate) {\n const dcpsid = this.dcpsid;\n var rejectErr;\n\n try\n {\n // Emit the close event the moment we know we are going to close, \n // so we can catch the close event and reopen the connection\n //\n // This implies that no API functions which call doClose may await between\n // their invocation and their call to doClose!\n this.emit('close', dcpsid /* should be undefined in initial state */);\n\n assert(this.state.in(['closing', 'close-wait']));\n if (preCloseState === 'established' && this.transport) {\n try {\n if (immediate) {\n await this.sendCloseImmediately(reason);\n } else {\n await this.sendCloseGracefully(reason);\n }\n } catch(e) {\n debugging() && console.warn(`Warning: could not send close message to peer. connectionid=${this.id}, dcpsid=,${this.dcpsid}, url=${this.url ? this.url.href : 'unknown url'} - (${e.message})`);\n }\n }\n\n // can delete these now that we've sent the close message\n this.dcpsid = null;\n this.peerAddress = null;\n\n if (reason instanceof DCPError)\n rejectErr = reason;\n else\n {\n let errorMessage = reason instanceof Error ? reason : `Connection to ${this.loggableDest} closed (${reason})`;\n rejectErr = new DCPError(errorMessage, 'DCPC-1013');\n }\n \n // Reject any pending transmissions in the message ledger\n this.messageLedger.failAllTransmissions(rejectErr);\n \n if (this.transport)\n {\n try { this.sender.shutdown(); }\n catch(e) { debugging() && console.warn(this.debugLabel, `Warning: could not shutdown sender; dcpsid=,${dcpsid}`, e); }\n \n try { this.transport.close(); delete this.transport; }\n catch(e) { debugging() && console.warn(this.debugLabel, `Warning: could not close transport; dcpsid=,${dcpsid}`, e); }\n }\n } catch(error) {\n debugging() && console.warn(this.debugLabel, `could not close connection; dcpsid=${dcpsid}, url=${this.url ? this.url.href : 'unknown url'}:`, error);\n }\n finally\n {\n this.state.set(['closing', 'close-wait'], 'closed');\n this.emit('end'); /* end event resolves promises on other threads for closeWait and close (ugh) */\n }\n }\n/**\n * Prepares a non-batchable message that can be sent directly over the wire. Returns when\n * the message has been signed and is ready to be sent. The connection will not be able to send \n * any messages until the prepared message here is either sent or discarded. If 'canBatch = true',\n * will return the unsigned message instead. In this case, enqueuing is handled by\n * `async Connection.send()`, allowing the message to be put in a batch before being signed.\n * @param {...any} messageData Data to build message with. Format is:\n * `operation {string}, \n * data {Object} (optional),\n * identity {wallet.Keystore} (optional),\n * canBatch {boolean} (optional)`\n * @returns {Promise<Object>} a promise which resolves to { message, signedMessage }\n */\n\n async prepare(...messageData)\n {\n if (this.state.isNot('established'))\n {\n await this.connect().catch((e) => {\n if (e.code !== 'DCPC-1015') /* If we're closed already, then swallow the error */\n { \n this.close(e, true);\n throw e;\n }\n });\n }\n \n \n let signedMessage, message = messageData[0];\n let canBatch = false;\n \n if (typeof messageData[messageData.length - 1] === 'boolean')\n canBatch = messageData.pop();\n \n if (!message.id)\n {\n message = this.Request.buildMessage(...messageData);\n }\n \n debugging('connection') && console.debug(`${this.debugLabel} Created message ${message.id}.`);\n \n message.ackToken = this.sender.makeAckToken();\n message.batchable = canBatch;\n \n if (canBatch)\n return Promise.resolve(message);\n \n debugging('connection') && console.debug(`${this.debugLabel} Preparing message ${message.id} for sending...`); \n const messageWithNonce = await new Promise((resolve) =>\n {\n // This event is fired in the sender by serviceQueue() when the message is at the top of the queue\n // and has a nonce it can sign with. At this point, we may return the prepared message.\n this.once(`${message.id} ready`, (message) => resolve(message))\n \n // if we're enqueing a graceful close message, delay until one pass of the event loop so we can handle pending messages \n if (message.payload.operation === 'close')\n setImmediate(() => { this.sender.queue.push(message); }); \n else\n this.sender.queue.push(message)\n \n this.sender.requestQueueService();\n })\n \n signedMessage = await messageWithNonce.sign();\n \n debugging('connection') && console.debug(`${this.debugLabel} Finished preparing message. ${message.id} is ready to be sent.`);\n \n return { message: messageWithNonce, signedMessage: signedMessage };\n }\n\n /**\n * Sends a message to the connected peer. If the connection has not yet been established,\n * this routine will first invoke this.connect(). If the first argument has a 'signedMessage'\n * property, the message is assumed to be prepared and is sent immediately. If not, and the first\n * argument does not have an 'id' property, it will be sent to `async prepare()`, and then put\n * in the message queue.\n * \n * @param {...any} args 3 forms:\n * [operation]\n * [operation, data]\n * [operation, data, identity]\n * @returns {Promise<Response>} a promise which resolves to a response.\n */\n async send(...args)\n {\n if (!this.state.is('established'))\n await this.connect().catch((e) =>\n {\n if (e.code !== 'DCPC-1015') /* If we're closed already, then swallow the error */\n { \n this.close(e, true);\n throw e;\n }\n });\n\n let message = args[0];\n // ie. already prepared\n if (message.signedMessage)\n return this.sendPreparedMessage(message);\n \n // ie. message not hyrdated or is a response, which needs ack token\n if (!message.id || message.type === 'response')\n message = await this.prepare(...args, true);\n\n if (this.state.in(['closed']))\n throw new DCPError(`Connection (${this.id}) is ${this.state}; cannot send. (${this.loggableDest})`, 'DCPC-1001');\n \n return this.sender.enqueue(message);\n }\n \n /**\n * Set the sender's flight deck with the given message and send it.\n * Can only be passed a prepared message, which is a promise that only\n * resolves to a message when it is signed with the nonce, so it must\n * be the next message to be sent (or discarded).\n * @param {Object} messageObject\n * @returns {Promise<Response>} \n */\n sendPreparedMessage(messageObject)\n {\n if (!messageObject.signedMessage) return;\n \n const { message, signedMessage } = messageObject;\n assert(!this.sender.inFlight);\n this.sender.inFlight = { message: message, signedMessage: signedMessage };\n const messageSentPromise = this.messageLedger.addMessage(message);\n this.sender.sendInFlightMessage();\n \n return messageSentPromise;\n }\n \n /**\n * Send a signed ack directly over the wire. If we get a SocketIO.Send: Not Connected error, \n * wait until we're connected and then resend the ack.\n * @param {String} ack \n */\n sendAck(ack)\n {\n try\n {\n this.transport.send(ack)\n }\n catch(error)\n {\n // Transport was lost\n if (error.code === 'DCPC-1105')\n this.once('connect', () => this.sendAck(ack));\n else\n console.error(`${this.debugLabel} Error acking message to ${this.loggableDest}: ${error}`);\n }\n }\n \n /**\n * Discard a prepared message by removing it from the queue.\n * Returns nonce to sender and provokes queue service.\n * @param {Object} messageObject { message, signedMessage } message to discard \n */\n discardMessage(messageObject)\n {\n let { message } = messageObject;\n this.sender.nonce = message.nonce;\n delete message.nonce;\n message.type = 'unhandled-message';\n this.sender.requestQueueService();\n }\n\n /**\n * This routine returns the current time for the purposes of\n * populating the Request message payload.validity.time property.\n * \n * @returns {Number} the integer number of seconds which have elapsed since the epoch\n */\n currentTime() {\n let msSinceEpoch;\n if (this.hasNtp) {\n msSinceEpoch = Date.now();\n } else {\n const msSinceLastReceipt = performance.now() - this.receiver.lastResponseTiming.receivedMs;\n msSinceEpoch = this.receiver.lastResponseTiming.time * 1000 + msSinceLastReceipt;\n }\n return Math.floor(msSinceEpoch / 1000);\n }\n\n /**\n * This method sends a keepalive to the peer, and resolves when the response has been received.\n */\n keepalive() {\n return this.send('keepalive');\n }\n}\n\n/** \n * Determine if we got the scheduler config from a secure source, eg https or local disk.\n * We assume tha all https transactions have PKI-CA verified.\n *\n * @note protocol::getSchedulerConfigLocation() is populated via node-libs/config.js or dcp-client/index.js\n *\n * @returns true or falsey\n */\nfunction determineIfSecureConfig()\n{\n var schedulerConfigLocation = (__webpack_require__(/*! dcp/protocol-v4 */ \"./src/protocol-v4/index.js\").getSchedulerConfigLocation)();\n var schedulerConfigSecure;\n\n if (schedulerConfigLocation && (schedulerConfigLocation.protocol === 'https:' || schedulerConfigLocation.protocol === 'file:'))\n {\n debugging('strict-mode') && console.debug(`scheduler config location ${schedulerConfigLocation} is secure`); /* from casual eavesdropping */\n schedulerConfigSecure = true;\n }\n\n if (isDebugBuild)\n {\n debugging('strict-mode') && console.debug('scheduler config location is always secure for debug builds');\n schedulerConfigSecure = 'debug';\n }\n\n debugging('strict-mode') && console.debug(`Config Location ${schedulerConfigLocation} is ${!schedulerConfigSecure ? 'not secure' : 'secure-' + schedulerConfigSecure}`);\n return schedulerConfigSecure;\n}\n\n/**\n * Determine if a URL is secure by examinining the protocol, connection, and information about the \n * process; in particular, we try to determine if the dcp config was securely provided, because if \n * it wasn't, then we can't have a secure location, since the origin could be compromised.\n * \n * \"Secure\" in this case means \"secure against casual eavesdropping\", and this information should only\n * be used to refuse to send secrets over the transport or similar.\n *\n * @returns true or falsey\n */\nfunction determineIfSecureLocation(conn)\n{\n var isSecureConfig = determineIfSecureConfig();\n var secureLocation;\n\n if (!isSecureConfig) /* can't have a secure location without a secure configuration */\n return null;\n \n if (isDebugBuild || conn.url.protocol === 'https:' || conn.url.protocol === 'tcps:')\n secureLocation = true;\n else if (conn.role === role.initiator && conn.target.hasOwnProperty('friendLocation') && conn.url === conn.target.friendLocation)\n secureLocation = true;\n else if (conn.options.allowUnencryptedSecrets)\n secureLocation = 'override';\n else\n secureLocation = false;\n\n debugging('strict-mode') && console.debug(`Location ${conn.url} is ${!secureLocation ? 'not secure' : 'secure-' + secureLocation}`);\n \n return secureLocation;\n}\nexports.Connection = Connection;\n\n\n//# sourceURL=webpack://dcp/./src/protocol-v4/connection/connection.js?");
|
|
4650
|
+
eval("/* provided dependency */ var process = __webpack_require__(/*! ./node_modules/process/browser.js */ \"./node_modules/process/browser.js\");\n/**\n * @file protocol/connection/connection.js\n * @author Ryan Rossiter\n * @author KC Erb\n * @author Wes Garland\n * @date January 2020, Feb 2021, Mar 2022\n *\n * A Connection object represents a connection to another DCP entity. \n * A DCP connection may 'live' longer than the underlying protocol's connection,\n * and the underlying protocol connection (or, indeed, protocol) may change\n * throughout the life of the DCP connection.\n * \n * DCP connections are uniquely identified by the DCP Session ID, specified by\n * the dcpsid property, present in every message body. This session id negotiated during connection,\n * with the initiator and target each providing half of the string.\n *\n * Connection instance events:\n * - session: dcpsid new session established\n * - connect: url UI hint: internet available\n * - disconnect: url UI hint: internet not available\n * - readyStateChange: *** DO NOT USE **\n * - error: error emitted when errors happen that would otherwise go uncaught\n * - close: connection instance is closing\n * - end: Connection instance is closed\n * - send: msgObj when a message is sent to the peer; does not wait for ack; may re-trigger on reconnect\n * - ready: when the connection is ready for traffic (constructor promises resolved)\n *\n * State Transition Diagram for Connection.state:\n *\n * initial connecting established disconnected close-wait closing closed\n * ===========================================================================================================================\n * |-- i:connect ---->\n * |-- t:accept ----->\n * |-- t:establishTarget -->\n * |-- i:connect ---------->\n * |-- transportDisconnectHandler -->\n * <-- i:_reconnect ----------------------------------------|\n * |-i:useNewTransport-->\n * <-- t:useNewTransport --------|\n * |-- closeWait ----------------------------------------------------------->\n * |-- closeWait ----------------------------------->\n * |-- closeWait -->\n * |-- doClose --------------->\n * |-- close ------------------------------------------------------------------------------------------------------------> \n * |-- close ---------------------------------------------------------------------------->\n * |-- close ---------------------------------------------------->\n * |-- close ------------------->\n * |-- doClose -->\n *\n *\n * Not until the established state can we count on things like a dcpsid, \n * peerAddress, identityPromise resolution and so on.\n * \n * Error Codes relevant to DCP Connections:\n * DCPC-1001 - CONNECTION CANNOT SEND WHEN IN CLOSING, CLOSE-WAIT OR CLOSED\n * EINVAL - MESSAGE OWNER IS INVALID (formerly DCPC-1002)\n * MESSAGE SIGNATURE INVALID (formerly DCPC-1003)\n * MESSAGE BODY IS INVALID (formerly DCPC-1004)\n * DCPC-1005 - TRYING TO ESTABLISH TARGET AFTER TARGET ALREADY ESTABLISHED\n * DCPC-1006 - CONNECTION COULD NOT BE ESTABLISHED WITHIN 30 SECONDS\n * DCPC-1007 - RECEIVED MESSAGE PAYLOAD BEFORE CONNECT OPERATION\n * DCPC-1008 - TARGET RESPONDED WITH INVALID DCPSID\n * DCPC-1009 - MESSAGE IS OF UNKNOWN TYPE\n * DCPC-1010 - DUPLICATE TRANSMISSION RECEIPT\n * DCPC-1011 - DEFAULT ERROR CODE WHEN PEER SENDS CLOSE MESSAGE\n * DCPC-1012 - TRIED TO INITIATE CONNECTION AFTER SESSION ALREADY ESTABLISHED\n * DCPC-1013 - DEFAULT ERROR CODE WHEN CLOSING WITH REASON THATS NOT INSTANCE OF DCPERROR\n * DCPC-1014 - NO TRANSPORTS AVAILABLE\n * DCPC-1015 - CANNOT CONNECT WHEN CONNECTION ALREADY CLOSED\n * DCPC-1016 - ERROR CONNECTING VIA AVAILABLE TRANSPORTS\n * DCPC-1017 - FIRST PROTOCOL MESSAGE WAS DID NOT INVOLVE INITIAL CONNECT REQUEST\n * DCPC-1018 - INVALID ARGUMENT PROVIDED IN PLACE OF IDKEYSTORE\n * ENODCPSID - CONNECTION INSTANCE TRIED TO RE-CONNECT TO A TARGET WHICH DOES NOT HAVE A RECORD OF THAT SESSION\n */\n\n\n\nconst debugging = (__webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope)('dcp');\nconst dcpEnv = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\nconst { EventEmitter } = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\");\nconst wallet = __webpack_require__(/*! dcp/dcp-client/wallet */ \"./src/dcp-client/wallet/index.js\");\nconst { DcpURL } = __webpack_require__(/*! dcp/common/dcp-url */ \"./src/common/dcp-url.js\");\nconst { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\nconst { assert } = __webpack_require__(/*! dcp/common/dcp-assert */ \"./src/common/dcp-assert.js\");\nconst { leafMerge, a$sleepMs } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\nconst { Synchronizer } = __webpack_require__(/*! dcp/common/concurrency */ \"./src/common/concurrency.js\");\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\n\nconst { Transport } = __webpack_require__(/*! ../transport */ \"./src/protocol-v4/transport/index.js\");\nconst { Sender } = __webpack_require__(/*! ./sender */ \"./src/protocol-v4/connection/sender.js\");\nconst { Receiver } = __webpack_require__(/*! ./receiver */ \"./src/protocol-v4/connection/receiver.js\");\nconst { MessageLedger } = __webpack_require__(/*! ./message-ledger */ \"./src/protocol-v4/connection/message-ledger.js\");\nconst { getGlobalIdentityCache } = __webpack_require__(/*! ./identity-cache */ \"./src/protocol-v4/connection/identity-cache.js\");\nconst { makeEBOIterator, setImmediateN, setImmediate } = __webpack_require__(/*! dcp/common/dcp-timers */ \"./src/common/dcp-timers.js\");\n\nconst { ConnectionMessage } = __webpack_require__(/*! ./connection-message */ \"./src/protocol-v4/connection/connection-message.js\");\nconst { ConnectionRequest } = __webpack_require__(/*! ./request */ \"./src/protocol-v4/connection/request.js\");\nconst { ConnectionResponse } = __webpack_require__(/*! ./response */ \"./src/protocol-v4/connection/response.js\");\nconst { ConnectionBatch } = __webpack_require__(/*! ./batch */ \"./src/protocol-v4/connection/batch.js\");\nconst { ConnectionAck } = __webpack_require__(/*! ./ack */ \"./src/protocol-v4/connection/ack.js\");\nconst { ErrorPayloadCtorFactory } = __webpack_require__(/*! ./error-payload */ \"./src/protocol-v4/connection/error-payload.js\");\nconst { role } = __webpack_require__(/*! ./connection-constants */ \"./src/protocol-v4/connection/connection-constants.js\");\n\nconst isDebugBuild = (__webpack_require__(/*! dcp/common/dcp-build */ \"./src/common/dcp-build.js\").build) === 'debug';\nlet nanoid;\nif (dcpEnv.platform === 'nodejs') {\n const { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\n nanoid = requireNative('nanoid').nanoid;\n} else {\n nanoid = (__webpack_require__(/*! nanoid */ \"./node_modules/nanoid/index.browser.js\").nanoid);\n}\n\n\nlet globalConnectionId = 0;\nvar _msgId = 0;\n\nconst CONNECTION_STATES = [\n 'initial',\n 'connecting', /* initiator: establish first transport instance connection; target: listening */\n 'established',\n 'disconnected', /* connection is still valid, but underlying transport is no longer connected */\n 'close-wait', /* Target of close message is in this state until response is acknowledged */\n 'closing',\n 'closed',\n]\n\nclass Connection extends EventEmitter {\n static get VERSION() {\n return '5.1.0'; // Semver format\n }\n\n static get VERSION_COMPATIBILITY() {\n return '^5.0.0'; // Semver format, can be a range\n }\n\n /**\n * Connection form 2:\n * @constructor\n * @param {object} [target]\n * @param {Promise} idKsPromise A promise which resolves to the identity keystore described\n * in form 1\n * @param {object} [options]\n * @see form 1\n */\n /**\n * Connection form 1\n * Create a DCP Connection object. This object could represent either the initiator or \n * target end of a connection, until it is specialized by either invoke the connect()\n * or accept() methods. Note that send() invokes connect() internally if not in an established\n * state.\n * @constructor\n * @note Connection objects exist for the lifetime of a given DCP connection \n * (session), whether or not the underlying transport (eg internet protocol) is connected or not. Once \n * the DCP session has ended, this object has no purpose and is not reusable.\n * \n * @param {object} target Object (usually a dcpConfig fragment) describing the target.\n * This object may contain the following properties; 'location' is\n * mandatory:\n * - location: a URL or DcpURL that is valid from the Internet\n * - friendLocation: a DcpURL that is valid from an intranet; if\n * both location and friendLocation specified, the best one will\n * be chosen by examining IP addresses (assuming an IP bearer).\n * - identity: an object with an address property which is a promise\n * that resolves to an instance of wallet.Address which represents\n * to the target's identity; this overrides the initiator's \n * identity cache unless options.strict is truey.\n * \n * @param {Keystore} [idKeystore] The keystore used to sign messages; used for non-repudiation.\n * If not specified, a dynamically-generated keystore will be used.\n * \n * @param {object} [options] Extra connection options that aren't defined via dcpConfig.dcp.connectionOptions.\n * These options include:\n * - identityUnlockTimeout: Number of (floating-point) seconds to leave the identity \n * keystore unlocked between invocations of Connection.send\n *\n * @returns instance of Connection that is specific to a target URL but not a role\n */\n constructor(target, idKeystore, options = {})\n {\n super('Connection');\n this.id = ++globalConnectionId;\n this.debugLabel = `connection(g:${this.id}):`;\n\n /* polymorphism strategy: rewrite to (configFragment, idksPromise, options) */\n if (target instanceof DcpURL)\n target = { location: target };\n else if (DcpURL.isURL(target))\n target = { location: new DcpURL(target) };\n else if (target instanceof String || typeof target === 'string')\n target = { location: new DcpURL(target) };\n assert(typeof target === 'object', target.location);\n\n /* idKeystore is always resolved by the time a session is established. */\n if (!idKeystore)\n this.identityPromise = wallet.getId();\n else if (idKeystore instanceof Promise)\n this.identityPromise = idKeystore;\n else if (idKeystore instanceof wallet.Keystore)\n this.identityPromise = Promise.resolve(idKeystore); \n else if (idKeystore instanceof wallet.Address)\n this.identityPromise = Promise.resolve(new wallet.Keystore(idKeystore, '')); \n else\n throw new DCPError('Invalid argument provided for IdKeystore', 'DCPC-1018');\n\n this.identityPromise.then((keystore) => {\n /* This always happens by the time a role is assumed */\n delete this.identityPromise;\n this.identity = keystore;\n this.emit('ready');\n debugging('connection') && console.debug(this.debugLabel, 'identity is', this.identity.address);\n });\n\n this.target = target;\n this.url = this.target.location;\n \n // Init internal state / vars\n this.state = new Synchronizer(CONNECTION_STATES[0], CONNECTION_STATES);\n // DO NOT USE this.state.on('change', (s) => this.emit('readyStateChange', s) );\n\n this.dcpsid = null;\n this.peerAddress = null;\n this.transport = null;\n this.messageLedger = new MessageLedger(this);\n this.authorizedSender = null;\n this.preDisconnectState = null;\n \n this.Message = ConnectionMessage(this);\n this.Request = ConnectionRequest(this.Message);\n this.Response = ConnectionResponse(this.Message);\n this.Batch = ConnectionBatch(this.Message);\n this.Ack = ConnectionAck(this.Message);\n this.ErrorPayload = ErrorPayloadCtorFactory(this);\n this.connectTime = Date.now();\n\n this.receiver = new Receiver(this);\n this.sender = new Sender(this);\n \n debugging('connection') && console.debug(this.debugLabel, `new connection#${this.id}; ${this.url}`);\n\n /* Create a connection config as this.options which takes into\n * account system defaults and overrides for specific urls, origins, etc.\n */\n this.options = leafMerge(\n ({ /* hardcoded defaults insulate us from missing web config */\n 'connectTimeout': 90,\n 'lingerTimeout': 1800,\n 'allowBatch': true,\n 'maxMessagesPerBatch': 100,\n 'identityUnlockTimeout': 300,\n 'ttl': {\n 'min': 15,\n 'max': 600,\n 'default': 120\n },\n 'transports': [ 'socketio' ],\n }),\n dcpConfig.dcp.connectionOptions.default,\n this.url && dcpConfig.dcp.connectionOptions[this.url.hostname],\n this.url && dcpConfig.dcp.connectionOptions[this.url.origin],\n options\n );\n\n /* draw out errors quickly in dev */\n if ((process.env.DCP_NETWORK_CONFIG_BUILD || dcpConfig.build) === 'debug')\n {\n this.options.maxMessagesPerBatch /= 10;\n \n /* short timeouts and debuggers don't get along well */\n if (dcpEnv.platform === 'nodejs' && !(requireNative('module')._cache.niim instanceof requireNative('module').Module))\n {\n this.options.connectTimeout /= 10;\n this.options.lingerTimeout /= 20;\n this.options.identityUnlockTimeout /= 10;\n }\n }\n\n assert(this.options.identityUnlockTimeout > 0);\n assert(this.options.connectTimeout > 0);\n assert(this.options.lingerTimeout > 0);\n assert(typeof this.options.ttl.min === 'number');\n assert(typeof this.options.ttl.max === 'number');\n assert(typeof this.options.ttl.default === 'number');\n \n this.backoffTimeIterator = makeEBOIterator(500, dcpConfig.build === 'debug' ? 5000 : 60000); /** XXXwg make this configurable */\n\n this.secureLocation = determineIfSecureLocation(this);\n this.loggableDest = '<generic>';\n }\n \n generateMessageId()\n {\n return `${this.id}-${_msgId++}-${Date.now()}-${nanoid()}`;\n }\n\n /**\n * Specialize an instance of Connection for either initiator or target behaviour. Once specialized,\n * the role cannot be changed. This happens based on methods invoked; connect() or accept() cause\n * the change.\n *\n * This specialization also implies that the connection is fully ready for use, including resolution\n * of the identity promise if necessary. This is perhaps not the best place to ensure that, but it\n * provides a reliable - and already async - waypoint to observe that event.\n */\n async a$assumeRole(myRole)\n {\n assert(myRole === role.initiator || myRole === role.target);\n\n if (this.role === myRole)\n return;\n this.role = myRole;\n \n if (this.role === role.target)\n {\n this.debugLabel = `connection(t:${this.id}):`;\n this.sender.debugLabel = `sender(t#${this.id}):`;\n this.messageLedger.debugLabel = `message-ledger(t#${this.id}):`;\n this.loggableDest = '<target>';\n this.hasNtp = true;\n }\n else\n {\n this.debugLabel = `connection(i:${this.id}):`;\n this.sender.debugLabel = `sender(i#${this.id}):`;\n this.messageLedger.debugLabel = `message-ledger(i#${this.id}):`;\n this.loggableDest = this.url.href;\n this.hasNtp = false;\n }\n\n debugging('connection') && console.debug(this.debugLabel, `connection #${this.id} is ${this.role} for ${this.url}`);\n if (!this.identity)\n {\n assert(this.identityPromise);\n debugging('connection') && console.debug(this.debugLabel, `waiting for identity resolution`);\n await this.identityPromise;\n }\n }\n\n /**\n * API to establish a DCP connection. Implied by send().\n *\n * When invoked by the initiator, this method establishes the connection by connecting\n * to the target url provided to the constructor.\n */\n async connect() // eslint-disable-line require-await\n {\n if (this.role == role.target)\n return;\n \n if (!this.role)\n await this.a$assumeRole(role.initiator);\n \n if (this.state.is('initial'))\n {\n if (!this.connectPromise)\n this.connectPromise = Promise.race([this.connectTimer(), this.a$_connect()]).then(() => { clearTimeout(this.connectTimeoutPromise); delete this.connectPromise });\n return this.connectPromise;\n }\n\n if (this.state.is('disconnected'))\n {\n if (!this.connectPromise)\n this.connectPromise = this.a$_reconnect().then(() => delete this.connectPromise);\n return this.connectPromise;\n }\n \n if (this.state.is('connecting'))\n {\n assert(this.connectPromise);\n return this.connectPromise;\n }\n\n if (this.state.is('established'))\n return;\n \n if (this.state.in(['closed', 'close-wait', 'closing']))\n throw new DCPError('Connection already closed', 'DCPC-1015');\n\n throw new Error('impossible');\n }\n\n /**\n * Performs a reconnection for connections which are in the disconnected state, and\n * tries to send any in-flight or enqueued messages as soon as that happens.\n */\n async a$_reconnect()\n {\n var transport;\n\n this.state.testAndSet('disconnected', 'connecting');\n try\n {\n do\n {\n transport = await this.a$connectToTargetTransport();\n } while (!transport && (this.transport && !this.transport.ready()) && !this.state.in(['closed', 'close-wait', 'closing']));\n\n debugging('connection') && console.debug(this.debugLabel, `reconnected via transport ${transport.socket.id}`);\n \n this.useNewTransport(transport);\n }\n catch (error)\n {\n if (error.code !== 'DCPC-1016' && error.code !== 'DCPC-1015')\n {\n /* Unreached unless there are bugs. */\n throw error;\n } \n this.close(error, true);\n }\n }\n\n connectTimer()\n {\n return new Promise((_resolve, reject) =>\n {\n var timeoutMs = (this.options.connectTimeout2 || 31) * 1000;\n \n this.connectTimeoutPromise = setTimeout(() =>\n { \n reject(new Error(`Failed to establish connection to ${this.url} within ${(timeoutMs / 1000).toFixed(1)} seconds`));\n }, timeoutMs);\n if (typeof this.connectTimeoutPromise.unref === 'function')\n this.connectTimeoutPromise.unref();\n });\n }\n \n async a$_connect()\n { \n var presharedPeerAddress, establishResults;\n var targetIdentity = await this.target.identity;\n var transport;\n \n assert(this.role === role.initiator);\n\n this.state.set('initial', 'connecting');\n do\n {\n transport = await this.a$connectToTargetTransport().catch((error) =>\n {\n debugging('connection') && console.debug(`${this.debugLabel} error connecting to target on transport layer:`, error);\n return { ready: () => {return false} };\n });\n } while(!transport.ready());\n this.adopt(transport);\n\n establishResults = await this.sender.establish().catch(error => {\n debugging('connection') && console.debug(this.debugLabel, `Could not establish DCP session ${this.transport ? 'over' + this.transport.name : '. Transport establishment was not complete'}:`, error);\n this.close(error, true);\n throw error;\n });\n const peerAddress = new wallet.Address(establishResults.peerAddress);\n const dcpsid = establishResults.dcpsid;\n debugging('connection') && console.debug(this.debugLabel, 'dcpsid is', dcpsid);\n \n if (!this.options.strict && targetIdentity && determineIfSecureConfig())\n {\n if ( false\n || typeof targetIdentity !== 'object'\n || typeof targetIdentity.address !== 'object'\n || !(targetIdentity.address instanceof wallet.Address))\n targetIdentity = { address: new wallet.Address(targetIdentity) }; /* map strings and Addresses to ks ducks */\n\n presharedPeerAddress = targetIdentity.address;\n debugging('connection') && console.debug(this.debugLabel, 'Using preshared peer address', presharedPeerAddress);\n }\n this.ensureIdentity(peerAddress, presharedPeerAddress);\n\n /* At this point, new session is valid & security checks out - make Connection instance usable */\n this.peerAddress = peerAddress;\n if (this.dcpsid)\n throw new DCPError(`Reached impossible state in connection.js; dcpsid already specified ${this.dcpsid} (${this.url})`, 'DCPC-1012');\n\n this.state.set('connecting', 'established'); /* established => dcpsid has been set */\n this.emit('session', (this.dcpsid = dcpsid));\n this.emit('connect', this.url);\n this.sender.notifyTransportReady();\n return Promise.resolve();\n }\n\n /**\n * unreference any objects entrained by this connection so that it does not prevent\n * the node program from exiting naturally.\n */\n unref()\n {\n if (this.connectAbortTimer && this.connectAbortTimer.unref && dcpEnv.platform === 'nodejs')\n this.connectAbortTimer.unref();\n }\n\n /**\n * Method is invoked when the transport disconnects. Transport instance is responsible for its own\n * finalization; Connection instance is responsible for finding a new transport, resuming the\n * connection, and retransmitting any in-flight message.\n *\n * @param {object} transport the transport instance that triggered this handler. In some cases, it\n * is possible that this event is not serviced until after the connection\n * has already acquired a new transport instance, e.g. in a Target where\n * the initiator switched networks. This implies that it is possible for\n * more 'connect' events to be emitted than 'disconnect' events.\n */\n transportDisconnectHandler(transport)\n {\n try\n { \n if (this.state.in(['disconnected', 'closing', 'close-wait', 'closed'])) /* transports may fire this more than once */\n return;\n\n if (transport !== this.transport) /* event no longer relevant */\n return;\n\n if (this.transport)\n {\n transport.close();\n delete this.transport;\n }\n \n if (this.state.is('established'))\n {\n this.state.set('established', 'disconnected');\n this.emit('disconnect', this.url); /* UI hint: \"internet unavailable\" */\n debugging('connection') && console.debug(this.debugLabel, `Transport disconnected from ${this.url}; ${this.sender.inFlight ? 'have' : 'no'} in-flight message`);\n \n if (!this.dcpsid) /* hopefully impossible? */\n {\n debugging('connection') && console.debug(this.debugLabel, 'Not reconnecting - no session');\n return;\n }\n }\n \n if (this.role === role.target)\n {\n /* targets generally can't reconnect due to NAT */\n debugging('connection') && console.debug(this.debugLabel, `Waiting for initiator to reconnect for ${this.dcpsid}`);\n return;\n }\n \n if (this.dcpsid && !this.sender.inFlight && this.options.onDemand)\n {\n debugging('connection') && console.debug(this.debugLabel, `Not reconnecting ${this.dcpsid} until next message`);\n return;\n }\n \n if (this.state.is('connecting') && (!this.dcpsid || !this.peerAddress))\n {\n debugging('connection') && console.debug(this.debugLabel, `Disconnected while connecting, establishing transport and re-sending connect request.`);\n this.a$_reconnect();\n return;\n }\n\n /* At this point, we initiate a (re)connect attempt because either\n * - we haven't connected yet,\n * - we have something to send, or\n * - we are not an on-demand connection\n */\n if (!this.state.is('connecting'))\n this.connect();\n }\n catch(error)\n {\n debugging('connection') && console.debug(this.debugLabel, 'unexpected error handling disconnect:', error);\n this.close(error, true);\n \n if (error.code !== 'DCPC-1016' && error.code !== 'DCPC-1015')\n {\n /* Unreached unless there are bugs. */\n throw error;\n }\n }\n }\n \n /**\n * Initiators only\n *\n * Connect to a target at the transport level.\n * - Rejects when we give up on all transports.\n * - Resolves with a transport instance when we connect to one.\n *\n * The connection attempt will keep a node program \"alive\" while it is happening.\n * The `autoUnref` connectionOption and unref() methods offer ways to make this not\n * happen.\n */\n async a$connectToTargetTransport()\n {\n const that = this;\n const availableTransports = [].concat(this.options.transports);\n var quitMsg = false; /* not falsey => reject asap, value is error message */\n var quitCode = undefined;\n var boSleepIntr; /* if not falsey, a function that interrupts the backoff sleep */\n var transportConnectIntr; /* if not falsey, a function that interrupts the current connection attempt */\n\n // Already trying to connect to target, don't try multiple times until we've aborted one attempt\n if (this.connectAbortTimer)\n return;\n \n /* This timer has the lifetime of the entire connection attempt. When we time out,\n * we set the quitMsg to get the retry loop to quit, then we interrupt the timer so\n * that we don't have to wait for the current backoff to expire before we notice, and\n * we expire the current attempt to connect right away as well.\n */\n this.connectAbortTimer = setTimeout(() => {\n quitMsg = 'connection timeout';\n if (boSleepIntr) boSleepIntr();\n if (transportConnectIntr) transportConnectIntr();\n }, this.options.connectTimeout * 1000);\n\n if (this.options.autoUnref)\n this.unref();\n\n /* cleanup code called on return/throw */\n function cleanup_ctt()\n {\n clearTimeout(that.connectAbortTimer);\n delete that.connectAbortTimer;\n }\n\n /* Connect to target with a specific transport. */\n /* Resolves with { bool success, obj transport } or rejects with { error } if the transport cannot connect*/\n function a$connectWithTransport(transportName)\n { \n transportConnectIntr = false;\n\n return new Promise((connectWithTransport_resolve, connectWithTransport_reject) => { \n const TransportClass = Transport.require(transportName);\n const transport = new TransportClass(that.target, that.options[transportName]);\n var ret = { transport };\n\n function cleanup_cwt()\n {\n for (let eventName of transport.eventNames())\n for (let listener of transport.listeners(eventName))\n transport.off(eventName, listener);\n }\n \n /* In the case where we have a race condition in the transport implementation, arrange things\n * so that we resolve with whatever fired last if we have a double-fire on the same pass of \n * the event loop.\n */\n transport.on('connect', () => { cleanup_cwt(); ret.success=true; connectWithTransport_resolve(ret) });\n transport.on('error', (error) => { cleanup_cwt(); connectWithTransport_reject(error) });\n transport.on('connect-failed', (error) => {\n cleanup_cwt();\n ret.success = false;\n ret.error = error;\n debugging() && console.debug(this.debugLabel, `Error connecting to ${that.url};`, error);\n connectWithTransport_resolve(ret);\n });\n \n /* let the connectAbortTimer interrupt this connect attempt */\n transportConnectIntr = () => { transport.close() };\n });\n }\n \n if (availableTransports.length === 0)\n {\n cleanup_ctt();\n return Promise.reject(new DCPError('no transports defined', 'DCPC-1014'));\n }\n \n /* Loop while trying each available transport in turn. Sleep with exponential backoff between runs */\n while (!quitMsg)\n {\n for (let transportName of availableTransports)\n {\n try\n {\n const { success, error, transport } = await a$connectWithTransport(transportName);\n\n /* Have connected to the remote at the transport level - OUT */\n if (success === true)\n {\n transportConnectIntr = false;\n cleanup_ctt();\n \n return transport;\n }\n\n /* Fast-fail: certain - but few - HTTP status codes let us know that this (or any) transport\n * will never work, so don't try those again.\n */\n if (error && error.httpStatus)\n {\n switch(error.httpStatus)\n {\n case 301: case 302: case 303: case 307: case 308:\n debugging('connection') && console.debug(this.debugLabel, `HTTP status ${error.httpStatus}; won't try again with ${transportName}`);\n availableTransports.splice(availableTransports.indexOf(transportName), 1);\n break;\n case 400: case 403: case 404:\n debugging('connection') && console.debug(this.debugLabel, `HTTP status ${error.httpStatus}; won't try again.`);\n quitMsg = error.message;\n quitCode = 'HTTP_' + error.httpStatus || 0;\n break;\n default:\n debugging('connection') && console.debug(this.debugLabel, `HTTP status ${error.httpStatus}; will try again with ${transportName}`);\n break;\n }\n }\n }\n catch (impossibleError)\n {\n /* transport connection attempts should never throw. */\n debugging('connection') && console.debug(this.debugLabel, `Error connecting to ${this.url} with ${transportName}; won't try again:`, impossibleError);\n availableTransports.splice(availableTransports.indexOf(transportName), 1);\n }\n }\n \n if (availableTransports.length === 0)\n {\n quitMsg = 'all transports exhausted';\n break;\n }\n \n /* Go to (interruptible) sleep for a while before trying again */\n const backoffTimeMs = this.backoffTimeIterator.next().value;\n debugging('connection') && console.debug(this.debugLabel, 'trying again in', Number(backoffTimeMs / 1000).toFixed(2), 'seconds');\n const boSleepPromise = a$sleepMs(backoffTimeMs);\n boSleepIntr = boSleepPromise.intr;\n await boSleepPromise;\n boSleepIntr = false;\n }\n\n /* The only way we get here is for us to discover that the connection is unconnectable - eg \n * reject timer has expired or similar.\n */\n cleanup_ctt();\n throw new DCPError(quitMsg, 'DCPC-1016', quitCode);\n }\n\n /**\n * Method which must be invoked whenever a new transport needs to be assigned to the connection. \n * If we have previously adopted a transport, we close it first, which will prevent \n * [by transport definition] any message handlers from firing, even if the old transport instance\n * has buffered traffic.\n *\n * @param {object} transport transport instance\n */\n adopt(transport)\n {\n if (this.transport)\n this.transport.close();\n\n transport.on('message', (m) => this.handleMessage(m));\n transport.on('end', () => this.transportDisconnectHandler(transport));\n transport.on('close', () => this.transportDisconnectHandler(transport));\n \n this.transport = transport;\n }\n \n \n /**\n * Method that gets invoked when there is a new transport available for adoption.\n * This will adjust the state of the connection, adopt the transport then tell\n * the sender to pump the message queue.\n * @param {object} transport transport instance \n */\n useNewTransport(transport)\n {\n if (this.state.in(['closing', 'close-wait', 'closed']))\n {\n debugging('connection') && console.debug(`${this.debugLabel} got a new transport during closing. closing the new transport and not completing transport adoption.`)\n transport.close();\n return;\n }\n \n var preDisconnectState = (!this.dcpsid || !this.peerAddress) ? 'connecting' : 'established';\n if (this.state.isNot(preDisconnectState))\n this.state.set(['connecting', 'disconnected'], preDisconnectState);\n this.adopt(transport);\n this.emit('connect', this.url); // UI hint: \"internet available\" \n this.sender.notifyTransportReady();\n }\n \n /**\n * Method that must be invoked by the target to \"accept\" a new DCP Connection request\n * at the transport layer.\n * @param {object} transport \n */\n async accept(transport)\n {\n assert(!this.role);\n await this.a$assumeRole(role.target);\n this.state.set('initial', 'connecting');\n this.adopt(transport);\n }\n \n /**\n * This method is invoked by the target when it has handled the initial connect request from\n * the initiator, which contains the peerAddress and the first half of the dcpsid (the second half is\n * populated by receiver.handleFirstRequest before being passed here). It transitions the connection \n * into an established state at the protocol level.\n * Note - this is really not the right design for this, but it is invoked from handleFirstRequest\n * in ./receiver.js\n *\n * @param {string} dcpsid dcpsid\n * @param {wallet.Address} peerAddress Address of peer\n */\n establishTarget(dcpsid, peerAddress) {\n assert(this.role === role.target);\n \n this.connectResponseId = Symbol('dummy'); // un-register ConnectResponse\n this.peerAddress = peerAddress;\n if (this.dcpsid)\n throw new DCPError(`Reached impossible state in connection.js; dcpsid already specified ${this.dcpsid}!=${dcpsid} (${this.url})`, 'DCPC-1005');\n this.emit('session', (this.dcpsid = dcpsid));\n debugging() && console.debug(this.debugLabel, 'dcpsid is', dcpsid);\n\n this.loggableDest = this.role === role.initiator ? this.url : peerAddress;\n this.state.set('connecting', 'established'); /* established => dcpsid has been set */\n\n debugging('connection') && console.debug(this.debugLabel, `Established session ${this.dcpsid} with ${this.peerAddress} for ${this.url}`);\n }\n\n /**\n * Check to see if the peer address conflicts with what we have in the global identity cache;\n * it does, throw an exception.\n */\n ensureIdentity (peerAddress, presharedPeerAddress)\n {\n let idc = getGlobalIdentityCache();\n let noConflict = idc.learnIdentity(this.url, peerAddress, presharedPeerAddress);\n\n if (!noConflict)\n throw new DCPError(`**** Security Error: Identity address ${peerAddress} does not match the saved key for ${this.url}`, 'DCPC-EADDRCHANGE');\n }\n \n \n /**\n * This method uses the first request (if we're target) or ack to the first request \n * (if we're initiator) to memoize the address of the peer authorized to send to us. \n * The first message must only be a request (since it comes from sender.specialFirstSend)\n * that has a connect operation, or an acknowledgement of the first request. \n * All future messages' owners will be validated against this authorized sender.\n * @param {Object} message \n */\n setAuthorizedSender(message)\n {\n if (message.body.type !== 'request' && message.body.type !== 'ack')\n throw new DCPError('First protocol message was not a request or ack', 'DCPC-1017');\n \n if (message.body.type === 'request' && message.body.payload.operation !== 'connect')\n throw new DCPError('First protocol message did not contain the correct payload', 'DCPC-1017');\n \n if (message.body.type === 'ack' && this.sender.inFlight.message.payload.operation !== 'connect')\n throw new DCPError('First protocol acknowledgement was not for connect request', 'DCPC-1017');\n \n this.authorizedSender = message.owner;\n }\n \n /**\n * Emits an error event with the relevant error and closes the connection immediately.\n * @param {string} errorMessage \n * @param {string} errorCode \n */\n \n handleValidationError(errorMessage, errorCode)\n {\n var messageError;\n \n debugging('connection') && console.debug(this.debugLabel, 'Message failed validation -', errorMessage);\n this.emit('error', (messageError = new DCPError(`message failed validation: ${errorMessage}`, errorCode)))\n this.close(messageError, true);\n }\n\n /**\n * This method validates the message owner, signature and body before passing it onto\n * either the receiver (for a request, response or batch) or the messageLedger (for an ack).\n * If it's a request, response or batch, this method also provokes the connection to \n * send an acknowledgement (ack) to the peer to let them know we got their message.\n * XXXwg this code needs an audit re error handling: what message error should we be emitting?\n * why do we keep working after we find an error?\n * XXXsc did some auditing. we happy now?\n * @param {string} JSON-encoded unvalidated message object\n */\n async handleMessage (messageJSON) {\n var validation;\n var message;\n\n if (this.state.is('closed')) {\n debugging('connection') && console.debug(this.debugLabel, 'handleMessage was called on a closed connection.');\n return;\n }\n\n try\n {\n message = typeof messageJSON === 'object' ? messageJSON : JSON.parse(messageJSON);\n debugging('wire') && console.debug(this.debugLabel, `handleMessage: ${String(message && message.body && message.body.type).padEnd(10, ' ')} <- ${this.loggableDest}`);\n }\n catch(error)\n {\n console.error('connection::handleMessage received unparseable message from peer:', error);\n this.emit('error', error);\n return;\n }\n \n /**\n * We always ack a duplicate transmission.\n * This must happen before validation since during startup we may lack a\n * nonce or dcpsid (depending on whether initiator or target + race).\n */\n if (this.isDuplicateTransmission(message)) {\n debugging('connection') && console.debug(this.debugLabel, 'duplicate message:', message.body);\n debugging('wire') && console.debug(this.debugLabel, `dup message ack: ${String(message.body.type).padEnd(10, ' ')} -> ${this.loggableDest}`);\n\n this.sendAck(this.lastAckSigned) \n return;\n }\n\n debugging('connection') && console.debug(this.debugLabel, `received message ${message.body.type} ${message.body.id}; nonce=`, message.body.nonce);\n \n validation = this.validateMessageDCPSID(message);\n if (validation.success !== true)\n {\n this.handleValidationError(validation.errorMessage, 'ENODCPSID');\n return;\n }\n\n validation = this.validateMessageOwner(message)\n if (validation.success !== true)\n {\n this.handleValidationError(validation.errorMessage, 'EINVAL');\n return;\n }\n \n validation = this.validateMessageSignature(message);\n if (validation.success !== true)\n {\n this.handleValidationError(validation.errorMessage, 'EINVAL');\n return;\n }\n\n validation = this.validateMessageBody(message);\n if (validation.success !== true)\n {\n this.handleValidationError(validation.errorMessage, validation.errorCode || 'EINVAL'); /* messages of type 'unhandled-message' may contain more information about the failure */\n return;\n }\n \n if (message.body.type === \"ack\") {\n const ack = new this.Ack(message.body);\n this.messageLedger.handleAck(ack);\n return;\n } else if (message.body.type !== 'unhandled-message') {\n this.lastMessage = message;\n await this.ackMessage(message);\n }\n \n this.receiver.handleMessage(message);\n }\n\n \n /**\n * This method takes either a Request, Response or Batch, creates an ack for it\n * and sends it to the peer. This ack contains the nonce we expect on the next\n * message from peer.\n * @param {Connection.Message} message \n */\n async ackMessage(message) {\n debugging('connection') && console.debug(this.debugLabel, 'acking message of type: ', message.body.type);\n const ack = new this.Ack(message);\n const signedMessage = await ack.sign(this.identity);\n\n debugging('wire') && console.debug(this.debugLabel, `ackMessage: ${String(message.body.type).padEnd(10, ' ')} -> ${this.loggableDest}`);\n\n this.sendAck(signedMessage);\n this.lastAck = ack;\n this.lastAckSigned = signedMessage;\n }\n\n /**\n * Checks if the batch we just received has the same nonce\n * as the most-recently received batch.\n * @param {object} messageJSON\n */\n isDuplicateTransmission(messageJSON) {\n return this.lastMessage && this.lastMessage.body.nonce && this.lastMessage.body.nonce === messageJSON.body.nonce;\n }\n\n /**\n * Validate that the message came from the appropriate sender.\n * @param {Object} message the message to validate\n * @returns {Object} returns an object `ret` with either ret.success = true, \n * or ret.success = false accompanied by another property ret.errorMessage\n */\n validateMessageOwner(message)\n {\n if (!this.authorizedSender)\n {\n /* Capture the initial identity of the remote end during the connect operation */\n this.setAuthorizedSender(message);\n return { success: true }\n }\n else if (message.owner !== this.authorizedSender)\n {\n return { success: false, errorMessage: \"message came from unauthorized sender\" }\n }\n return { success: true }\n }\n \n /**\n * Validate that the signature was generated from this message body\n * @param {Object} message\n * @returns {Object} with properties 'success' and 'errorMessage'. When the message is valid on its \n * face, the success property is true, otherwise it is is false. When it is false,\n * the errorMessage property will be a string explaining why.\n */\n validateMessageSignature(message)\n {\n if (!message.signature) {\n debugging('connection') && console.warn(this.debugLabel, \"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(this.debugLabel, \"Message has an invalid signature, aborting connection\");\n return { success: false, errorMessage: \"invalid message signature\" };\n }\n\n return { success: true };\n }\n \n validateMessageDCPSID(message)\n {\n if (this.dcpsid !== null && message.dcpsid)\n {\n if (message.dcpsid !== this.dcpsid)\n {\n debugging('connection') && console.warn(this.debugLabel, 'Message has an invalid dcpsid, aborting connection');\n return { success: false, errorMessage: 'message has an invalid dcpsid' };\n }\n }\n \n return { success: true };\n }\n /**\n * This method is used to perform validation on all types of messages.\n * It validates the DCPSID, nonce, and the peerAddress.\n * @param {Object} message\n * @returns {Object} with properties 'success' and 'errorMessage'. When the message is valid on its \n * face, the success property is true, otherwise it is is false. When it is false,\n * the errorMessage property will be a string explaining why.\n *\n */\n validateMessageBody(message)\n {\n try\n {\n if (message.body.type === 'unhandled-message')\n {\n /* This special message type may not have a dcpsid, peerAddress, etc., so it might not\n * validate. It is also not a \"real\" message and only used to report ConnectionManager routing \n * errors, so we just report here, drop it, and close the connection.\n *\n * Note also that this is probably the wrong way to handle this case - restarting daemons - but\n * that is a problem for another day. /wg nov 2021\n */\n debugging('connection') && console.warn(this.debugLabel, \"Target Error - target could not process message.\", JSON.stringify(message.body),\n \"Aborting connection.\");\n return { success: false, errorMessage: `target could not process message (${message.body.payload && message.body.payload.message || 'unknown error'})`, errorCode: message.body.payload && message.body.payload.code}\n }\n if (this.peerAddress && !this.peerAddress.eq(message.owner))\n {\n debugging('connection') && console.warn(this.debugLabel,\n \"Received message's owner address does not match peer address, aborting connection\\n\",\n \"(owner addr)\", message.owner, '\\n',\n \"(peer addr)\", this.peerAddress);\n return { success: false, errorMessage: \"received message owner does not match peer address\" };\n }\n\n if (this.state.in(['established', 'closing', 'close-wait']) && message.body.type !== 'unhandled-message')\n {\n const body = message.body;\n\n assert(this.peerAddress); /* should be set in connect */\n /**\n * Security note:\n * We don't require the dcpsid to match on an ack because the connect response\n * ack doesn't have a dcpsid until after it is processed. Also ack's are protected\n * by ack tokens and signatures, so this doesn't leave a hole, just an inconsistency.\n */\n if (body.type !== 'ack' && body.dcpsid !== this.dcpsid)\n {\n debugging('connection') && console.warn(this.debugLabel,\n \"Received message's DCPSID does not match, aborting connection\\n\",\n \"Message owner:\", message.owner, '\\n',\n \"(ours)\", this.dcpsid, (Date.now() - this.connectTime)/1000, \"seconds after connecting - state:\", this.state._, \"\\n\", \n \"(theirs)\", body.dcpsid);\n if(body.dcpsid.substring(0, body.dcpsid.length/2) !== this.dcpsid.substring(0, this.dcpsid.length/2)){\n debugging('connection') && console.warn(this.debugLabel, \" 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(this.debugLabel, \" Right half of both DCPSID is different\");\n }\n return { success: false, errorMessage: \"DCPSID do not match\" };\n }\n /* can get close in middle of connecting, which will have no nonce.*/\n if (body.type !== 'ack' && this.lastAck.nonce !== body.nonce && (body.payload && body.payload.operation !== 'close'))\n {\n /* When Target sends back ConnectionLessErrorResponse, it uses the nonce of the message that caused an error. */\n if (this.sender.inFlight && this.sender.inFlight.message.nonce === body.nonce)\n {\n debugging('connection') && console.debug(`${this.debugLabel} Received messages nonce matches nonce of our current inFlight message.`,\n \"There was a problem sending this message. Aborting connection. Reason:\\n\", body.payload);\n return { success: false, errorMessage: \"current inflight message returned an error\" }\n }\n debugging('connection') && console.warn(this.debugLabel,\"Received message's nonce does not match expected nonce, aborting connection\\n\");\n debugging('connection') && console.debug(this.debugLabel, this.lastAck.nonce, body.nonce);\n return { success: false, errorMessage: \"received message's nonce does not match expected nonce\" };\n }\n if (body.type === 'request') \n {\n if (body.payload.validity.time === undefined)\n return { success: false, errorMessage: 'received message does not have a valid time in its payload' };\n }\n }\n\n return { success: true };\n }\n catch(error)\n {\n console.error('message validator failure:', error);\n return { success: false, errorMessage: 'validator exception ' + error.message };\n }\n\n return { success: false, errorMessage: 'impossible code reached' }; // eslint-disable-line no-unreachable\n }\n\n /**\n * Targets Only.\n * The receiver creates a special connect response and the connection\n * needs to know about it to get ready for the ack. See `isWaitingForAck`.\n * @param {Message} message message we are sending out and waiting to\n * ack'd, probably a batch containing the response.\n */\n registerConnectResponse(message) {\n this.connectResponseId = message.id;\n }\n\n /**\n * Targets only\n * During the connection process a target sends a connect\n * response to an initiator and the initiator will ack it. Since transports\n * are not tightly coupled, we have no authoritative way to route the ack back\n * to the right connection. So a connection briefly registers the ack it\n * is looking for in this case. It will formally validate the ack after routing.\n * @param {string} messageId id of the message this ack is acknowledging.\n */\n isWaitingForAck(messageId) {\n return messageId === this.connectResponseId;\n }\n\n /**\n * Put connection into close-wait state so that a call to `close`\n * in this state will *not* trigger sending a `close` message to the peer.\n * Then call close.\n *\n * @note: This function is called when the remote end of the transport sends\n * a close command, from receiver::handleOperation. This impllies that\n * that we must be in established or later state.\n */\n closeWait (errorCode = null)\n {\n var preCloseState, reason;\n \n debugging('connection') && console.debug(this.debugLabel, `responding to close. state=closeWait dcpsid=${this.dcpsid}`);\n\n if (this.state.is('closed'))\n {\n debugging('connection') && console.debug(this.debugLabel, `remote asked us to close a closed connection; dcpsid=${this.dcpsid}`);\n return;\n }\n\n // continue with close in either case\n reason = `Received close from peer with Error Code ${errorCode}`;\n if (this.role === role.target)\n reason += ` (${this.url})`;\n else\n reason += ` (${this.debugLabel}${this.peerAddress.address})`;\n\n reason = new DCPError(reason, errorCode || 'DCPC-1011');\n\n // If we're already closing, wait for it to complete then resolve\n // WARNING: any place we transition to closing or close-wait, we MUST guarantedd\n // that 'end' will be emitted, or this code will hang forever!\n if (this.state.in(['close-wait', 'closing'])) {\n return new Promise((resolve) => {\n this.once('end', resolve) /* eventually fired by doClose elsewhere */\n });\n }\n\n /* XXXwg - this should only be established->close-wait. Why more? */\n this.state.set(['disconnected', 'connecting', 'established'], 'close-wait');\n \n /* Set preCloseState to close-wait so doClose doesn't send a close message back */\n preCloseState = this.state.valueOf();\n return this.doClose(preCloseState, reason, true);\n }\n\n /**\n * This method will begin closing the protocol connection. It transitions\n * the protocol into the correct state, and then begins the work of closing.\n * \n * @param {string|Error} [reason] Either an Error or a message to use in the Error that will reject pending sends.\n * @param {boolean} [immediate] When true, the connection will not deliver any pending messages and instead\n * immediately send the peer a 'close' request. \n *\n * @return a Promise which resolves when the connection has been confirmed closed and the end event has been fired.\n */\n close (reason='requested', immediate=false)\n {\n if (this.state.is('initial'))\n {\n debugging('connection') && console.debug(this.debugLabel, 'not yet established; closing directly.');\n this.state.set('initial', 'closed');\n this.emit('close'); /* Don't emit dcpsid */\n }\n if (this.state.is('closed')) return Promise.resolve();\n\n const preCloseState = this.state.valueOf();\n debugging('connection') && \n console.debug(this.debugLabel, \n `close; dcpsid=${this.dcpsid} state=${preCloseState} immediate=${immediate} reason:`, reason);\n\n // If we're already closing, wait for it to complete then resolve\n if (this.state.in(['close-wait', 'closing'])) {\n debugging('connection') && console.debug(this.debugLabel, 'already closing; awaiting `end` event...');\n return new Promise((resolve) => {\n this.once('end', resolve)\n });\n }\n\n this.state.set(['connecting', 'established', 'disconnected'], 'closing');\n\n // Perform actual work of closing\n return this.doClose(preCloseState, reason, immediate);\n }\n\n /**\n * Sends close message to peer after sending all pending messages.\n * Note that close messages are sent without the expectation of a response.\n * @param {DCPError|string} reason reason for closing\n */\n async sendCloseGracefully(reason) \n {\n debugging('connection') && console.debug(`${this.debugLabel} gracefully sending close message to peer with reason ${reason}`)\n let errorCode = reason instanceof DCPError ? reason.code : 'DCPC-1011';\n \n /* This only resolves when close is next message in queue */\n const closeMessage = await this.prepare('close', { errorCode: errorCode });\n debugging('connection') && console.debug(`1249: close message prepared; sending...`);\n this.sendPreparedMessage(closeMessage);\n this.messageLedger.fulfillMessagePromise(closeMessage.message.id, {});\n debugging('connection') && console.debug(`1251: close message prepared; sending...`);\n }\n \n /**\n * Sends close message to peer immediately. Pending messages will not be sent.\n * Note that close messages are sent without expectation of response.\n * @param {DCPError|string} reason reason for closing\n */\n async sendCloseImmediately(reason)\n {\n debugging('connection') && console.debug(`${this.debugLabel} immediately sending close message to peer with reason ${reason}`);\n let errorCode = reason instanceof DCPError ? reason.code : 'DCPC-1011';\n \n /* Last param being `true` means that prepareMessage will return unsigned message. Does not queue message. */\n const closeMessage = await this.prepare('close', { errorCode: errorCode }, true);\n \n if (this.sender.inFlight)\n closeMessage.nonce = this.sender.inFlight.message.nonce;\n else\n closeMessage.nonce = this.sender.nonce;\n \n let signedCloseMessage = await closeMessage.sign();\n \n /* Overwrite the in-flight message because we don't care to deliver pending messages */\n this.sender.inFlight = { message: closeMessage, signedMessage: signedCloseMessage };\n this.sender.sendInFlightMessage();\n }\n \n /**\n * This method performs the core close functionality. It appropriately sends the close message\n * to the peer, fails any pending transmissions, shuts down our sender and underlying transport\n * and puts us into the 'closed' state, indicating this connection object is now useless.\n * When called from closeWait, it does not send a close message.\n * @param {string} preCloseState the state that the connection was in at the start of the\n * invocation of close() or closeWait()\n *\n * @note: this function is not reentrant due to closeGracefully\n */\n async doClose(preCloseState, reason, immediate) {\n const dcpsid = this.dcpsid;\n var rejectErr;\n\n try\n {\n // Emit the close event the moment we know we are going to close, \n // so we can catch the close event and reopen the connection\n //\n // This implies that no API functions which call doClose may await between\n // their invocation and their call to doClose!\n this.emit('close', dcpsid /* should be undefined in initial state */);\n\n assert(this.state.in(['closing', 'close-wait']));\n if (preCloseState === 'established' && this.transport) {\n try {\n if (immediate) {\n await this.sendCloseImmediately(reason);\n } else {\n await this.sendCloseGracefully(reason);\n }\n } catch(e) {\n debugging() && console.warn(this.debugLabel, `Warning: could not send close message to peer. connectionid=${this.id}, dcpsid=,${this.dcpsid}, url=${this.url ? this.url.href : 'unknown url'} - (${e.message})`);\n }\n }\n\n // can delete these now that we've sent the close message\n this.dcpsid = null;\n this.peerAddress = null;\n\n if (reason instanceof DCPError)\n rejectErr = reason;\n else\n {\n let errorMessage = reason instanceof Error ? reason : `Connection to ${this.loggableDest} closed (${reason})`;\n rejectErr = new DCPError(errorMessage, 'DCPC-1013');\n }\n \n // Reject any pending transmissions in the message ledger\n this.messageLedger.failAllTransmissions(rejectErr);\n \n if (this.transport)\n {\n try { this.sender.shutdown(); }\n catch(e) { debugging() && console.warn(this.debugLabel, `Warning: could not shutdown sender; dcpsid=,${dcpsid}`, e); }\n \n try { this.transport.close(); delete this.transport; }\n catch(e) { debugging() && console.warn(this.debugLabel, `Warning: could not close transport; dcpsid=,${dcpsid}`, e); }\n }\n } catch(error) {\n debugging() && console.warn(this.debugLabel, `could not close connection; dcpsid=${dcpsid}, url=${this.url ? this.url.href : 'unknown url'}:`, error);\n }\n finally\n {\n this.state.set(['closing', 'close-wait'], 'closed');\n this.emit('end'); /* end event resolves promises on other threads for closeWait and close (ugh) */\n }\n }\n/**\n * Prepares a non-batchable message that can be sent directly over the wire. Returns when\n * the message has been signed and is ready to be sent. The connection will not be able to send \n * any messages until the prepared message here is either sent or discarded. If 'canBatch = true',\n * will return the unsigned message instead. In this case, enqueuing is handled by\n * `async Connection.send()`, allowing the message to be put in a batch before being signed.\n * @param {...any} messageData Data to build message with. Format is:\n * `operation {string}, \n * data {Object} (optional),\n * identity {wallet.Keystore} (optional),\n * canBatch {boolean} (optional)`\n * @returns {Promise<Object>} a promise which resolves to { message, signedMessage }\n */\n\n async prepare(...messageData)\n {\n if (this.state.isNot('established'))\n {\n await this.connect().catch((e) => {\n if (e.code !== 'DCPC-1015') /* If we're closed already, then swallow the error */\n { \n this.close(e, true);\n throw e;\n }\n });\n }\n \n \n let signedMessage, message = messageData[0];\n let canBatch = false;\n \n if (typeof messageData[messageData.length - 1] === 'boolean')\n canBatch = messageData.pop();\n \n if (!message.id)\n {\n message = this.Request.buildMessage(...messageData);\n }\n \n debugging('connection') && console.debug(`${this.debugLabel} Created message ${message.id}.`);\n \n message.ackToken = this.sender.makeAckToken();\n message.batchable = canBatch;\n \n if (canBatch)\n return Promise.resolve(message);\n \n debugging('connection') && console.debug(`${this.debugLabel} Preparing message ${message.id} for sending...`); \n const messageWithNonce = await new Promise((resolve) =>\n {\n // This event is fired in the sender by serviceQueue() when the message is at the top of the queue\n // and has a nonce it can sign with. At this point, we may return the prepared message.\n this.once(`${message.id} ready`, (message) => resolve(message))\n \n // if we're enqueing a graceful close message, delay until one pass of the event loop so we can handle pending messages \n if (message.payload.operation === 'close')\n setImmediate(() => { \n this.sender.queue.push(message);\n this.sender.requestQueueService();\n }); \n else\n this.sender.queue.push(message)\n \n this.sender.requestQueueService();\n })\n \n signedMessage = await messageWithNonce.sign();\n \n debugging('connection') && console.debug(`${this.debugLabel} Finished preparing message. ${message.id} is ready to be sent.`);\n \n return { message: messageWithNonce, signedMessage: signedMessage };\n }\n\n /**\n * Sends a message to the connected peer. If the connection has not yet been established,\n * this routine will first invoke this.connect(). If the first argument has a 'signedMessage'\n * property, the message is assumed to be prepared and is sent immediately. If not, and the first\n * argument does not have an 'id' property, it will be sent to `async prepare()`, and then put\n * in the message queue.\n * \n * @param {...any} args 3 forms:\n * [operation]\n * [operation, data]\n * [operation, data, identity]\n * @returns {Promise<Response>} a promise which resolves to a response.\n */\n async send(...args)\n {\n if (!this.state.is('established'))\n await this.connect().catch((e) =>\n {\n if (e.code !== 'DCPC-1015') /* If we're closed already, then swallow the error */\n { \n this.close(e, true);\n throw e;\n }\n });\n\n let message = args[0];\n // ie. already prepared\n if (message.signedMessage)\n return this.sendPreparedMessage(message);\n \n // ie. message not hyrdated or is a response, which needs ack token\n if (!message.id || message.type === 'response')\n message = await this.prepare(...args, true);\n\n if (this.state.in(['closed']))\n throw new DCPError(`Connection (${this.id}) is ${this.state}; cannot send. (${this.loggableDest})`, 'DCPC-1001');\n \n return this.sender.enqueue(message);\n }\n \n /**\n * Set the sender's flight deck with the given message and send it.\n * Can only be passed a prepared message, which is a promise that only\n * resolves to a message when it is signed with the nonce, so it must\n * be the next message to be sent (or discarded).\n * @param {Object} messageObject\n * @returns {Promise<Response>} \n */\n sendPreparedMessage(messageObject)\n {\n if (!messageObject.signedMessage) return;\n \n const { message, signedMessage } = messageObject;\n assert(!this.sender.inFlight);\n this.sender.inFlight = { message: message, signedMessage: signedMessage };\n const messageSentPromise = this.messageLedger.addMessage(message);\n this.sender.sendInFlightMessage();\n \n return messageSentPromise;\n }\n \n /**\n * Send a signed ack directly over the wire. If we get a SocketIO.Send: Not Connected error, \n * wait until we're connected and then resend the ack.\n * @param {String} ack \n */\n sendAck(ack)\n {\n try\n {\n this.transport.send(ack)\n }\n catch(error)\n {\n // Transport was lost\n if (error.code === 'DCPC-1105')\n this.once('connect', () => this.sendAck(ack));\n else\n console.error(`${this.debugLabel} Error acking message to ${this.loggableDest}: ${error}`);\n }\n }\n \n /**\n * Discard a prepared message by removing it from the queue.\n * Returns nonce to sender and provokes queue service.\n * @param {Object} messageObject { message, signedMessage } message to discard \n */\n discardMessage(messageObject)\n {\n let { message } = messageObject;\n this.sender.nonce = message.nonce;\n delete message.nonce;\n message.type = 'unhandled-message';\n this.sender.requestQueueService();\n }\n\n /**\n * This routine returns the current time for the purposes of\n * populating the Request message payload.validity.time property.\n * \n * @returns {Number} the integer number of seconds which have elapsed since the epoch\n */\n currentTime() {\n let msSinceEpoch;\n if (this.hasNtp) {\n msSinceEpoch = Date.now();\n } else {\n const msSinceLastReceipt = performance.now() - this.receiver.lastResponseTiming.receivedMs;\n msSinceEpoch = this.receiver.lastResponseTiming.time * 1000 + msSinceLastReceipt;\n }\n return Math.floor(msSinceEpoch / 1000);\n }\n\n /**\n * This method sends a keepalive to the peer, and resolves when the response has been received.\n */\n keepalive() {\n return this.send('keepalive');\n }\n}\n\n/** \n * Determine if we got the scheduler config from a secure source, eg https or local disk.\n * We assume tha all https transactions have PKI-CA verified.\n *\n * @note protocol::getSchedulerConfigLocation() is populated via node-libs/config.js or dcp-client/index.js\n *\n * @returns true or falsey\n */\nfunction determineIfSecureConfig()\n{\n var schedulerConfigLocation = (__webpack_require__(/*! dcp/protocol-v4 */ \"./src/protocol-v4/index.js\").getSchedulerConfigLocation)();\n var schedulerConfigSecure;\n\n if (schedulerConfigLocation && (schedulerConfigLocation.protocol === 'https:' || schedulerConfigLocation.protocol === 'file:'))\n {\n debugging('strict-mode') && console.debug(`connection: scheduler config location ${schedulerConfigLocation} is secure`); /* from casual eavesdropping */\n schedulerConfigSecure = true;\n }\n\n if (isDebugBuild)\n {\n debugging('strict-mode') && console.debug('connection: scheduler config location is always secure for debug builds');\n schedulerConfigSecure = 'debug';\n }\n\n debugging('strict-mode') && console.debug(`connection: Config Location ${schedulerConfigLocation} is ${!schedulerConfigSecure ? 'not secure' : 'secure-' + schedulerConfigSecure}`);\n return schedulerConfigSecure;\n}\n\n/**\n * Determine if a URL is secure by examinining the protocol, connection, and information about the \n * process; in particular, we try to determine if the dcp config was securely provided, because if \n * it wasn't, then we can't have a secure location, since the origin could be compromised.\n * \n * \"Secure\" in this case means \"secure against casual eavesdropping\", and this information should only\n * be used to refuse to send secrets over the transport or similar.\n *\n * @returns true or falsey\n */\nfunction determineIfSecureLocation(conn)\n{\n var isSecureConfig = determineIfSecureConfig();\n var secureLocation;\n\n if (!isSecureConfig) /* can't have a secure location without a secure configuration */\n return null;\n \n if (isDebugBuild || conn.url.protocol === 'https:' || conn.url.protocol === 'tcps:')\n secureLocation = true;\n else if (conn.role === role.initiator && conn.target.hasOwnProperty('friendLocation') && conn.url === conn.target.friendLocation)\n secureLocation = true;\n else if (conn.options.allowUnencryptedSecrets)\n secureLocation = 'override';\n else\n secureLocation = false;\n\n debugging('strict-mode') && console.debug(`connection: Location ${conn.url} is ${!secureLocation ? 'not secure' : 'secure-' + secureLocation}`);\n \n return secureLocation;\n}\nexports.Connection = Connection;\n\n\n//# sourceURL=webpack://dcp/./src/protocol-v4/connection/connection.js?");
|
|
4651
4651
|
|
|
4652
4652
|
/***/ }),
|
|
4653
4653
|
|