dcp-client 4.2.9 → 4.2.12

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.
@@ -251,6 +251,16 @@ eval("// Currently in sync with Node.js lib/internal/util/comparisons.js\n// htt
251
251
 
252
252
  /***/ }),
253
253
 
254
+ /***/ "./node_modules/atob/browser-atob.js":
255
+ /*!*******************************************!*\
256
+ !*** ./node_modules/atob/browser-atob.js ***!
257
+ \*******************************************/
258
+ /***/ ((module, __unused_webpack_exports, __webpack_require__) => {
259
+
260
+ eval("/* module decorator */ module = __webpack_require__.nmd(module);\n/* provided dependency */ var Buffer = __webpack_require__(/*! ./node_modules/node-polyfill-webpack-plugin/node_modules/buffer/index.js */ \"./node_modules/node-polyfill-webpack-plugin/node_modules/buffer/index.js\")[\"Buffer\"];\n(function (w) {\n \"use strict\";\n\n function findBest(atobNative) {\n // normal window\n if ('function' === typeof atobNative) { return atobNative; }\n\n\n // browserify (web worker)\n if ('function' === typeof Buffer) {\n return function atobBrowserify(a) {\n //!! Deliberately using an API that's deprecated in node.js because\n //!! this file is for browsers and we expect them to cope with it.\n //!! Discussion: github.com/node-browser-compat/atob/pull/9\n return new Buffer(a, 'base64').toString('binary');\n };\n }\n\n // ios web worker with base64js\n if ('object' === typeof w.base64js) {\n // bufferToBinaryString\n // https://git.coolaj86.com/coolaj86/unibabel.js/blob/master/index.js#L50\n return function atobWebWorker_iOS(a) {\n var buf = w.base64js.b64ToByteArray(a);\n return Array.prototype.map.call(buf, function (ch) {\n return String.fromCharCode(ch);\n }).join('');\n };\n }\n\n\t\treturn function () {\n\t\t\t// ios web worker without base64js\n\t\t\tthrow new Error(\"You're probably in an old browser or an iOS webworker.\" +\n\t\t\t\t\" It might help to include beatgammit's base64-js.\");\n };\n }\n\n var atobBest = findBest(w.atob);\n w.atob = atobBest;\n\n if (( true) && module && module.exports) {\n module.exports = atobBest;\n }\n}(window));\n\n\n//# sourceURL=webpack://dcp/./node_modules/atob/browser-atob.js?");
261
+
262
+ /***/ }),
263
+
254
264
  /***/ "./node_modules/backo2/index.js":
255
265
  /*!**************************************!*\
256
266
  !*** ./node_modules/backo2/index.js ***!
@@ -3831,7 +3841,13 @@ eval("// Copyright Joyent, Inc. and other Node contributors.\n//\n// Permission
3831
3841
  /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
3832
3842
 
3833
3843
  "use strict";
3834
- eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": () => (/* binding */ Modal)\n/* harmony export */ });\n/**\n * A Small Modal Class\n * @module Modal\n */\n/* globals Event dcpConfig */\nclass Modal {\n constructor (title, message, callback = false, exitHandler = false, {\n continueLabel = 'Continue',\n cancelLabel = 'Cancel',\n cancelVisible = true\n } = {}) {\n const modal = document.createElement('div')\n modal.className = 'dcp-modal-container-old day'\n modal.innerHTML = `\n <dialog class=\"dcp-modal-content\">\n <div class=\"dcp-modal-header\">\n <h2>${title}<button type=\"button\" class=\"close\">&times;</button></h2>\n ${message ? '<p>' + message + '</p>' : ''}\n </div>\n <div class=\"dcp-modal-loading hidden\">\n <div class='loading'></div>\n </div>\n <form onsubmit='return false' method=\"dialog\">\n <div class=\"dcp-modal-body\"></div>\n <div class=\"dcp-modal-footer ${cancelVisible ? '' : 'centered'}\">\n <button type=\"submit\" class=\"continue green-modal-button\">${continueLabel}</button>\n <button type=\"button\" class=\"cancel green-modal-button\">${cancelLabel}</button>\n </div>\n </form>\n </dialog>`\n\n // To give a reference to do developer who wants to override the form submit.\n // May occur if they want to validate the information in the backend\n // without closing the modal prematurely.\n this.form = modal.querySelector('.dcp-modal-content form')\n this.continueButton = modal.querySelector('.dcp-modal-footer button.continue')\n this.cancelButton = modal.querySelector('.dcp-modal-footer button.cancel')\n this.closeButton = modal.querySelector('.dcp-modal-header .close')\n if (!cancelVisible) {\n this.cancelButton.style.display = 'none'\n }\n\n // To remove the event listener, the reference to the original function\n // added is required.\n this.formSubmitHandler = this.continue.bind(this)\n\n modal.addEventListener('keydown', function (event) {\n event.stopPropagation()\n // 27 is the keycode for the escape key.\n if (event.keyCode === 27) this.close()\n }.bind(this))\n\n this.container = modal\n this.callback = callback\n this.exitHandler = exitHandler\n document.body.appendChild(modal)\n }\n\n changeFormSubmitHandler (newFormSubmitHandler) {\n this.formSubmitHandler = newFormSubmitHandler\n }\n\n /**\n * Validates the form values in the modal and calls the modal's callback\n */\n async continue (event) {\n // To further prevent form submission from trying to redirect from the\n // current page.\n if (event instanceof Event) {\n event.preventDefault()\n }\n let fieldsAreValid = true\n let formElements = this.container.querySelectorAll('.dcp-modal-body select, .dcp-modal-body input, .dcp-modal-body textarea')\n\n const formValues = []\n if (typeof formElements.length === 'undefined') formElements = [formElements]\n // Separate into two loops to enable input validation requiring formValues\n // that come after it. e.g. Two password fields matching.\n for (let i = 0; i < formElements.length; i++) {\n switch (formElements[i].type) {\n case 'file':\n formValues.push(formElements[i])\n break\n case 'checkbox':\n formValues.push(formElements[i].checked)\n break\n default:\n formValues.push(formElements[i].value)\n break\n }\n }\n for (let i = 0; i < formElements.length; i++) {\n if (formElements[i].validation) {\n // Optional fields are allowed to be empty but still can't be wrong if not empty.\n if (!(formElements[i].value === '' && !formElements[i].required)) {\n if (typeof formElements[i].validation === 'function') {\n if (!formElements[i].validation(formValues)) {\n fieldsAreValid = false\n formElements[i].classList.add('is-invalid')\n }\n } else if (!formElements[i].validation.test(formElements[i].value)) {\n fieldsAreValid = false\n formElements[i].classList.add('is-invalid')\n }\n }\n }\n }\n\n if (!fieldsAreValid) return\n\n this.loading()\n if (typeof this.callback === 'function') {\n try {\n return this.callback(formValues)\n } catch (error) {\n console.error('Unexpected error in modal.continue:', error);\n return this.close(false)\n }\n }\n this.close(true)\n }\n\n loading () {\n this.container.querySelector('.dcp-modal-loading').classList.remove('hidden')\n this.container.querySelector('.dcp-modal-body').classList.add('hidden')\n this.container.querySelector('.dcp-modal-footer').classList.add('hidden')\n }\n\n open () {\n this.form.addEventListener('submit', async (event) => {\n const success = await this.formSubmitHandler(event)\n if (success === false) {\n return\n }\n this.close(true)\n })\n // When the user clicks on <span> (x), close the modal\n this.closeButton.addEventListener('click', this.close.bind(this))\n this.cancelButton.addEventListener('click', this.close.bind(this))\n\n // Prevent lingering outlines after clicking some form elements.\n this.container.querySelectorAll('.dcp-modal-body button, .dcp-modal-body input[type=\"checkbox\"]').forEach(element => {\n element.addEventListener('click', () => {\n element.blur()\n })\n })\n\n // Show the modal.\n this.container.style.display = 'block'\n\n const formElements = this.container.querySelectorAll('.dcp-modal-body select, .dcp-modal-body input')\n if (formElements.length) {\n formElements[0].focus()\n if (formElements[0].type === 'text') {\n formElements[0].select()\n }\n for (const el of formElements) {\n if (el.realType) {\n el.type = el.realType\n }\n }\n } else {\n // With no form elements to allow for form submission on enter, focus the\n // continue button.\n this.container.querySelector('.dcp-modal-footer button.continue').focus()\n }\n } // TODO: This should return a promise with the action resolving it\n\n /**\n * Shows the modal and returns a promise of the result of the modal (e.g. was\n * it closed, did its action succeed?)\n */\n showModal () {\n return new Promise((resolve, reject) => {\n this.form.addEventListener('submit', handleContinue.bind(this))\n this.cancelButton.addEventListener('click', handleCancel.bind(this))\n this.closeButton.addEventListener('click', handleCancel.bind(this))\n\n // Prevent lingering outlines after clicking some form elements.\n this.container.querySelectorAll('.dcp-modal-body button, .dcp-modal-body input[type=\"checkbox\"]').forEach(element => {\n element.addEventListener('click', () => {\n element.blur()\n })\n })\n\n // Show the modal.\n this.container.style.display = 'block'\n\n const formElements = this.container.querySelectorAll('.dcp-modal-body select, .dcp-modal-body input')\n if (formElements.length) {\n formElements[0].focus()\n if (formElements[0].type === 'text') {\n formElements[0].select()\n }\n for (const el of formElements) {\n if (el.realType) {\n el.type = el.realType\n }\n }\n } else {\n // With no form elements to allow for form submission on enter, focus the\n // continue button.\n this.continueButton.focus()\n }\n\n async function handleContinue (event) {\n let result\n try {\n result = await this.formSubmitHandler(event)\n } catch (error) {\n reject(error)\n }\n this.close(true)\n resolve(result)\n }\n\n async function handleCancel () {\n let result\n try {\n result = await this.close()\n } catch (error) {\n reject(error)\n }\n resolve(result)\n }\n })\n }\n\n close (success = false) {\n this.container.style.display = 'none'\n if (this.container.parentNode) {\n this.container.parentNode.removeChild(this.container)\n }\n\n // @todo this needs to remove eventlisteners to prevent memory leaks\n\n if ((success !== true) && typeof this.exitHandler === 'function') {\n return this.exitHandler(this)\n }\n }\n\n /**\n * Adds different form elements to the modal depending on the case.\n *\n * @param {*} elements - The properties of the form elements to add.\n * @returns {HTMLElement} The input form elements.\n */\n addFormElement (...elements) {\n const body = this.container.querySelector('.dcp-modal-body')\n const inputElements = []\n let label\n for (let i = 0; i < elements.length; i++) {\n let row = document.createElement('div')\n row.className = 'row'\n\n let col, input\n switch (elements[i].type) {\n case 'button':\n col = document.createElement('div')\n col.className = 'col-md-12'\n\n input = document.createElement('button')\n input.innerHTML = elements[i].label\n input.type = 'button'\n input.classList.add('green-modal-button')\n if (!elements[i].onclick) {\n throw new Error('A button in the modal body should have an on click event handler.')\n }\n input.addEventListener('click', elements[i].onclick)\n\n col.appendChild(input)\n row.appendChild(col)\n break\n case 'textarea':\n col = document.createElement('div')\n col.className = 'col-md-12'\n\n input = document.createElement('textarea')\n input.className = 'text-input-field form-control'\n if (elements[i].placeholder) input.placeholder = elements[i].placeholder\n\n col.appendChild(input)\n row.appendChild(col)\n break\n case 'text':\n case 'email':\n case 'number':\n case 'password': {\n const inputCol = document.createElement('div')\n\n input = document.createElement('input')\n input.type = elements[i].type\n input.validation = elements[i].validation\n input.autocomplete = elements[i].autocomplete || (elements[i].type === 'password' ? 'off' : 'on')\n input.className = 'text-input-field form-control'\n\n // Adding bootstraps custom feedback styles.\n let invalidFeedback = null\n if (elements[i].invalidFeedback) {\n invalidFeedback = document.createElement('div')\n invalidFeedback.className = 'invalid-feedback'\n invalidFeedback.innerText = elements[i].invalidFeedback\n }\n\n if (elements[i].type === 'password') {\n elements[i].realType = 'password'\n }\n\n if (elements[i].label) {\n const labelCol = document.createElement('div')\n label = document.createElement('label')\n label.innerText = elements[i].label\n const inputId = 'dcp-modal-input-' + this.container.querySelectorAll('input[type=\"text\"], input[type=\"email\"], input[type=\"number\"], input[type=\"password\"]').length\n label.setAttribute('for', inputId)\n input.id = inputId\n labelCol.classList.add('col-md-6', 'label-column')\n labelCol.appendChild(label)\n row.appendChild(labelCol)\n inputCol.className = 'col-md-6'\n } else {\n inputCol.className = 'col-md-12'\n }\n\n inputCol.appendChild(input)\n if (invalidFeedback !== null) {\n inputCol.appendChild(invalidFeedback)\n }\n row.appendChild(inputCol)\n break\n }\n case 'select':\n col = document.createElement('div')\n col.className = 'col-md-4'\n\n label = document.createElement('span')\n label.innerText = elements[i].label\n\n col.appendChild(label)\n row.appendChild(col)\n\n col = document.createElement('div')\n col.className = 'col-md-8'\n\n input = document.createElement('select')\n\n col.appendChild(input)\n row.appendChild(col)\n break\n case 'checkbox': {\n row.classList.add('checkbox-row')\n const checkboxLabelCol = document.createElement('div')\n checkboxLabelCol.classList.add('label-column', 'checkbox-label-column')\n\n label = document.createElement('label')\n label.innerText = elements[i].label\n label.for = 'dcp-checkbox-input-' + this.container.querySelectorAll('input[type=\"checkbox\"]').length\n label.setAttribute('for', label.for)\n label.className = 'checkbox-label'\n\n checkboxLabelCol.appendChild(label)\n\n const checkboxCol = document.createElement('div')\n checkboxCol.classList.add('checkbox-column')\n\n input = document.createElement('input')\n input.type = 'checkbox'\n input.id = label.for\n if (elements[i].checked) {\n input.checked = true\n }\n\n checkboxCol.appendChild(input)\n\n if (elements[i].labelToTheRightOfCheckbox) {\n checkboxCol.classList.add('col-md-5')\n row.appendChild(checkboxCol)\n checkboxLabelCol.classList.add('col-md-7')\n row.appendChild(checkboxLabelCol)\n } else {\n checkboxLabelCol.classList.add('col-md-6')\n checkboxCol.classList.add('col-md-6')\n row.appendChild(checkboxLabelCol)\n row.appendChild(checkboxCol)\n }\n break\n }\n case 'file':\n [input, row] = this.addFileInput(elements[i], input, row)\n break\n case 'label':\n row.classList.add('label-row')\n label = document.createElement('label')\n label.innerText = elements[i].label\n row.appendChild(label)\n break\n }\n\n // Copy other possibly specified element properties:\n const inputPropertyNames = ['title', 'inputmode', 'value', 'minLength', 'maxLength', 'size', 'required', 'pattern', 'min', 'max', 'step', 'placeholder', 'accept', 'multiple', 'id', 'onkeypress', 'oninput', 'for', 'readonly', 'autocomplete']\n for (const propertyName of inputPropertyNames) {\n if (Object.prototype.hasOwnProperty.call(elements[i], propertyName)) {\n if (propertyName === 'for' && !label.hasAttribute(propertyName)) {\n label.setAttribute(propertyName, elements[i][propertyName])\n }\n if (propertyName.startsWith('on')) {\n input.addEventListener(propertyName.slice(2), elements[i][propertyName])\n } else {\n input.setAttribute(propertyName, elements[i][propertyName])\n }\n }\n }\n\n inputElements.push(input)\n body.appendChild(row)\n }\n\n if (inputElements.length === 1) return inputElements[0]\n else return inputElements\n }\n\n /**\n * Adds a drag and drop file form element to the modal.\n *\n * @param {*} fileInputProperties - An object specifying some of the\n * properties of the file input element.\n * @param {*} fileInput - Placeholders to help create the file\n * input.\n * @param {HTMLDivElement} row - Placeholders to help create the file\n * input.\n */\n addFileInput (fileInputProperties, fileInput, row) {\n // Adding the upload label.\n const uploadLabel = document.createElement('label')\n uploadLabel.innerText = fileInputProperties.label\n row.appendChild(uploadLabel)\n const body = this.container.querySelector('.dcp-modal-body')\n body.appendChild(row)\n const fileSelectionRow = document.createElement('div')\n fileSelectionRow.id = 'file-selection-row'\n\n // Adding the drag and drop file upload input.\n const dropContainer = document.createElement('div')\n dropContainer.id = 'drop-container'\n\n // Adding an image of a wallet\n const imageContainer = document.createElement('div')\n imageContainer.id = 'image-container'\n const walletImage = document.createElement('span')\n walletImage.classList.add('fas', 'fa-wallet')\n imageContainer.appendChild(walletImage)\n\n // Adding some text prompts\n const dropMessage = document.createElement('span')\n dropMessage.innerText = 'Drop a keystore file here'\n const orMessage = document.createElement('span')\n orMessage.innerText = 'or'\n\n // Adding the manual file input element (hiding the default one)\n const fileInputContainer = document.createElement('div')\n const fileInputLabel = document.createElement('label')\n // Linking the label to the file input so that clicking on the label\n // activates the file input.\n fileInputLabel.setAttribute('for', 'file-input')\n fileInputLabel.innerText = 'Browse'\n fileInput = document.createElement('input')\n fileInput.type = fileInputProperties.type\n fileInput.id = 'file-input'\n // To remove the lingering outline after selecting the file.\n fileInput.addEventListener('click', () => {\n fileInput.blur()\n })\n fileInputContainer.append(fileInput, fileInputLabel)\n\n // Creating the final row element to append to the modal body.\n dropContainer.append(imageContainer, dropMessage, orMessage, fileInputContainer)\n fileSelectionRow.appendChild(dropContainer)\n\n // Adding functionality to the drag and drop file input.\n dropContainer.addEventListener('drop', selectDroppedFile.bind(this))\n dropContainer.addEventListener('drop', unhighlightDropArea)\n // Prevent file from being opened by the browser.\n dropContainer.ondragover = highlightDropArea\n dropContainer.ondragenter = highlightDropArea\n dropContainer.ondragleave = unhighlightDropArea\n\n fileInput.addEventListener('change', handleFileChange)\n\n const fileNamePlaceholder = document.createElement('center')\n fileNamePlaceholder.id = 'file-name-placeholder'\n fileNamePlaceholder.className = 'row'\n fileNamePlaceholder.innerText = ''\n fileSelectionRow.appendChild(fileNamePlaceholder)\n fileNamePlaceholder.classList.add('hidden')\n\n // Check if the continue button is invalid on the keystore upload modal and\n // click it if it should no longer be invalid.\n this.continueButton.addEventListener('invalid', () => {\n const fileFormElements = this.container.querySelectorAll('.dcp-modal-body input[type=\"file\"], .dcp-modal-body input[type=\"text\"]')\n const filledInFileFormElements = Array.from(fileFormElements).filter(fileFormElement => fileFormElement.value !== '')\n if (fileFormElements.length !== 0 && filledInFileFormElements.length !== 0) {\n this.continueButton.setCustomValidity('')\n // Clicking instead of dispatching a submit event to ensure other form validation is used before submitting the form.\n this.continueButton.click()\n }\n })\n\n return [fileInput, fileSelectionRow]\n\n /**\n * Checks that the dropped items contain only a single keystore file.\n * If valid, sets the file input's value to the dropped file.\n * @param {DragEvent} event - Contains the files dropped.\n */\n function selectDroppedFile (event) {\n // Prevent file from being opened.\n event.preventDefault()\n\n // Check if only one file was dropped.\n const wasOneFileDropped = event.dataTransfer.items.length === 1 ||\n event.dataTransfer.files.length === 1\n updateFileSelectionStatus(wasOneFileDropped)\n if (!wasOneFileDropped) {\n fileInput.setCustomValidity('Only one file can be uploaded.')\n fileInput.reportValidity()\n return\n } else {\n fileInput.setCustomValidity('')\n }\n\n // Now to use the DataTransfer interface to access the file(s), setting\n // the value of the file input.\n const file = event.dataTransfer.files[0]\n\n if (checkFileExtension(file)) {\n fileInput.files = event.dataTransfer.files\n fileInput.dispatchEvent(new Event('change'))\n }\n }\n\n function handleFileChange () {\n if (checkFileExtension(this.files[0]) && this.files.length === 1) {\n fileNamePlaceholder.innerText = `Selected File: ${this.files[0].name}`\n updateFileSelectionStatus(true)\n // Invoke a callback if additional functionality is required.\n if (typeof fileInputProperties.callback === 'function') {\n fileInputProperties.callback(this.files[0])\n }\n }\n }\n\n /**\n * Checks if the file extension on the inputted file is correct.\n * @param {File} file - The file to check\n * @returns {boolean} True if the file extension is valid, false otherwise.\n */\n function checkFileExtension (file) {\n // If there's no restriction, return true.\n if (!fileInputProperties.extension) {\n return true\n }\n const fileExtension = file.name.split('.').pop()\n const isValidExtension = fileExtension === fileInputProperties.extension\n updateFileSelectionStatus(isValidExtension)\n if (!isValidExtension) {\n fileInput.setCustomValidity(`Only a .${fileInputProperties.extension} file can be uploaded.`)\n fileInput.reportValidity()\n fileNamePlaceholder.classList.add('hidden')\n } else {\n fileInput.setCustomValidity('')\n }\n return isValidExtension\n }\n\n /**\n * Updates the file input to reflect the validity of the current file\n * selection.\n * @param {boolean} isValidFileSelection - True if a single .keystore file\n * was selected. False otherwise.\n */\n function updateFileSelectionStatus (isValidFileSelection) {\n imageContainer.innerHTML = ''\n const statusImage = document.createElement('span')\n statusImage.classList.add('fas', isValidFileSelection ? 'fa-check' : 'fa-times')\n statusImage.style.color = isValidFileSelection ? 'green' : 'red'\n imageContainer.appendChild(statusImage)\n\n if (!isValidFileSelection) {\n fileInput.value = null\n fileNamePlaceholder.classList.add('hidden')\n } else {\n fileNamePlaceholder.classList.remove('hidden')\n }\n\n // If the modal contains a password field for a keystore file, change its\n // visibility.\n const walletPasswordInputContainer = document.querySelector('.dcp-modal-body input[type=\"password\"]').parentElement.parentElement\n if (walletPasswordInputContainer) {\n if (isValidFileSelection) {\n walletPasswordInputContainer.classList.remove('hidden')\n const walletPasswordInput = document.querySelector('.dcp-modal-body input[type=\"password\"]')\n walletPasswordInput.focus()\n } else {\n walletPasswordInputContainer.classList.add('hidden')\n }\n }\n }\n\n function highlightDropArea (event) {\n event.preventDefault()\n this.classList.add('highlight')\n }\n\n function unhighlightDropArea (event) {\n event.preventDefault()\n this.classList.remove('highlight')\n }\n }\n\n /**\n * Sets up a custom tooltip to pop up when the passwords do not match, but are\n * valid otherwise.\n */\n addFormValidationForPasswordConfirmation () {\n const [newPassword, confirmPassword] = document.querySelectorAll('.dcp-modal-body input[type=\"password\"]')\n if (!newPassword || !confirmPassword) {\n throw Error('New Password field and Confirm Password fields not present.')\n }\n\n newPassword.addEventListener('input', checkMatchingPasswords)\n confirmPassword.addEventListener('input', checkMatchingPasswords)\n\n function checkMatchingPasswords () {\n if (newPassword.value !== confirmPassword.value &&\n newPassword.validity.valid &&\n confirmPassword.validity.valid) {\n newPassword.setCustomValidity('Both passwords must match.')\n } else if (newPassword.value === confirmPassword.value ||\n newPassword.validity.tooShort ||\n newPassword.validity.patternMismatch ||\n newPassword.validity.valueMissing ||\n confirmPassword.validity.tooShort ||\n confirmPassword.validity.patternMismatch ||\n confirmPassword.validity.valueMissing) {\n // If the passwords fields match or have become invalidated some other\n // way again, reset the custom message.\n newPassword.setCustomValidity('')\n }\n }\n }\n\n updateInvalidEmailMessage() {\n const email = document.querySelector('.dcp-modal-body input[id=\"email\"')\n if (!email){\n throw Error(\"Email field not present\")\n }\n email.addEventListener('input', checkValidEmail);\n function checkValidEmail() {\n if (!email.validity.patternMismatch &&\n !email.validity.valueMissing) {\n email.setCustomValidity('')\n } else {\n email.setCustomValidity(\"Enter a valid email address.\")\n }\n\n }\n }\n\n /**\n * Adds message(s) to the modal's body.\n * @param {string} messages - The message(s) to add to the modal's body.\n * @returns Paragraph element(s) containing the message(s) added to the\n * modal's body.\n */\n addMessage (...messages) {\n const elements = []\n const body = this.container.querySelector('.dcp-modal-body')\n for (let i = 0; i < messages.length; i++) {\n const row = document.createElement('div')\n row.className = 'row'\n\n const paragraph = document.createElement('p')\n paragraph.innerHTML = messages[i]\n paragraph.classList.add('message')\n row.appendChild(paragraph)\n body.appendChild(row)\n\n elements.push(paragraph)\n }\n\n if (elements.length === 1) return elements[0]\n else return elements\n }\n\n addHorizontalRule () {\n const body = this.container.querySelector('.dcp-modal-body')\n body.appendChild(document.createElement('hr'))\n }\n\n // Does what it says. Still ill advised to use unless you have to.\n addCustomHTML (htmlStr, browseCallback) {\n const elements = []\n const body = this.container.querySelector('.dcp-modal-body')\n body.innerHTML += htmlStr\n body.querySelector('#browse-button').addEventListener('click', browseCallback.bind(this, this))\n\n if (elements.length === 1) return elements[0]\n else return elements\n }\n\n addButton (...buttons) {\n const elements = []\n const body = this.container.querySelector('.dcp-modal-body')\n for (let i = 0; i < buttons.length; i++) {\n const row = document.createElement('div')\n row.className = 'row'\n\n let col = document.createElement('div')\n col.className = 'col-md-4'\n\n const description = document.createElement('span')\n description.innerText = buttons[i].description\n\n col.appendChild(description)\n row.appendChild(col)\n\n col = document.createElement('div')\n col.className = 'col-md-8'\n\n const button = document.createElement('button')\n button.innerText = buttons[i].label\n button.addEventListener('click', buttons[i].callback.bind(this, this))\n\n elements.push(button)\n\n col.appendChild(button)\n row.appendChild(col)\n\n body.appendChild(row)\n }\n\n if (elements.length === 1) return elements[0]\n else return elements\n }\n}\n\n\n// Inject our special stylesheet from dcp-client only if we're on the portal webpage.\nif (typeof window !== 'undefined' && typeof document !== 'undefined' && dcpConfig.portal.location.hostname === window.location.hostname) {\n // <link rel='stylesheet' href='/css/dashboard.css'>\n const stylesheet = document.createElement('link')\n stylesheet.rel = 'stylesheet'\n // Needed for the duplicate check done later.\n stylesheet.id = 'dcp-modal-styles'\n\n const dcpClientBundle = document.getElementById('_dcp_client_bundle')\n let src\n if (dcpClientBundle) {\n src = dcpClientBundle.src.replace('dcp-client-bundle.js', 'dcp-modal-style.css')\n } else {\n src = dcpConfig.portal.location.href + 'dcp-client/dist/dcp-modal-style.css'\n }\n\n stylesheet.href = src\n // If the style was injected before, don't inject it again.\n // Could occur when loading a file that imports Modal.js and loading\n // comput.min.js in the same HTML file.\n if (document.getElementById(stylesheet.id) === null) {\n document.getElementsByTagName('head')[0].appendChild(stylesheet)\n }\n\n if (typeof {\"version\":\"b5864d51c1e7a9b3024698306574b60003a91d62\",\"branch\":\"release\",\"dcpClient\":{\"version\":\"4.2.9\",\"from\":\"git+ssh://git@gitlab.com/Distributed-Compute-Protocol/dcp-client.git#prod-20220718\",\"resolved\":\"git+ssh://git@gitlab.com/Distributed-Compute-Protocol/dcp-client.git#15ef46ae27df39b0d1383bd282d36f8c3227905e\"},\"built\":\"Fri Jul 22 2022 09:34:47 GMT-0400 (Eastern Daylight Saving Time)\",\"config\":{\"generated\":\"Fri 22 Jul 2022 09:34:45 AM EDT by erose on lorge\",\"build\":\"debug\"},\"webpack\":\"5.70.0\",\"node\":\"v14.20.0\"} !== 'undefined' && typeof window.Modal === 'undefined') {\n window.Modal = Modal\n }\n}\n\n\n//# sourceURL=webpack://dcp/./portal/www/js/modal.js?");
3844
+ <<<<<<< HEAD
3845
+ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": () => (/* binding */ Modal)\n/* harmony export */ });\n/**\n * A Small Modal Class\n * @module Modal\n */\n/* globals Event dcpConfig */\nclass Modal {\n constructor (title, message, callback = false, exitHandler = false, {\n continueLabel = 'Continue',\n cancelLabel = 'Cancel',\n cancelVisible = true\n } = {}) {\n const modal = document.createElement('div')\n modal.className = 'dcp-modal-container-old day'\n modal.innerHTML = `\n <dialog class=\"dcp-modal-content\">\n <div class=\"dcp-modal-header\">\n <h2>${title}<button type=\"button\" class=\"close\">&times;</button></h2>\n ${message ? '<p>' + message + '</p>' : ''}\n </div>\n <div class=\"dcp-modal-loading hidden\">\n <div class='loading'></div>\n </div>\n <form onsubmit='return false' method=\"dialog\">\n <div class=\"dcp-modal-body\"></div>\n <div class=\"dcp-modal-footer ${cancelVisible ? '' : 'centered'}\">\n <button type=\"submit\" class=\"continue green-modal-button\">${continueLabel}</button>\n <button type=\"button\" class=\"cancel green-modal-button\">${cancelLabel}</button>\n </div>\n </form>\n </dialog>`\n\n // To give a reference to do developer who wants to override the form submit.\n // May occur if they want to validate the information in the backend\n // without closing the modal prematurely.\n this.form = modal.querySelector('.dcp-modal-content form')\n this.continueButton = modal.querySelector('.dcp-modal-footer button.continue')\n this.cancelButton = modal.querySelector('.dcp-modal-footer button.cancel')\n this.closeButton = modal.querySelector('.dcp-modal-header .close')\n if (!cancelVisible) {\n this.cancelButton.style.display = 'none'\n }\n\n // To remove the event listener, the reference to the original function\n // added is required.\n this.formSubmitHandler = this.continue.bind(this)\n\n modal.addEventListener('keydown', function (event) {\n event.stopPropagation()\n // 27 is the keycode for the escape key.\n if (event.keyCode === 27) this.close()\n }.bind(this))\n\n this.container = modal\n this.callback = callback\n this.exitHandler = exitHandler\n document.body.appendChild(modal)\n }\n\n changeFormSubmitHandler (newFormSubmitHandler) {\n this.formSubmitHandler = newFormSubmitHandler\n }\n\n /**\n * Validates the form values in the modal and calls the modal's callback\n */\n async continue (event) {\n // To further prevent form submission from trying to redirect from the\n // current page.\n if (event instanceof Event) {\n event.preventDefault()\n }\n let fieldsAreValid = true\n let formElements = this.container.querySelectorAll('.dcp-modal-body select, .dcp-modal-body input, .dcp-modal-body textarea')\n\n const formValues = []\n if (typeof formElements.length === 'undefined') formElements = [formElements]\n // Separate into two loops to enable input validation requiring formValues\n // that come after it. e.g. Two password fields matching.\n for (let i = 0; i < formElements.length; i++) {\n switch (formElements[i].type) {\n case 'file':\n formValues.push(formElements[i])\n break\n case 'checkbox':\n formValues.push(formElements[i].checked)\n break\n default:\n formValues.push(formElements[i].value)\n break\n }\n }\n for (let i = 0; i < formElements.length; i++) {\n if (formElements[i].validation) {\n // Optional fields are allowed to be empty but still can't be wrong if not empty.\n if (!(formElements[i].value === '' && !formElements[i].required)) {\n if (typeof formElements[i].validation === 'function') {\n if (!formElements[i].validation(formValues)) {\n fieldsAreValid = false\n formElements[i].classList.add('is-invalid')\n }\n } else if (!formElements[i].validation.test(formElements[i].value)) {\n fieldsAreValid = false\n formElements[i].classList.add('is-invalid')\n }\n }\n }\n }\n\n if (!fieldsAreValid) return\n\n this.loading()\n if (typeof this.callback === 'function') {\n try {\n return this.callback(formValues)\n } catch (error) {\n console.error('Unexpected error in modal.continue:', error);\n return this.close(false)\n }\n }\n this.close(true)\n }\n\n loading () {\n this.container.querySelector('.dcp-modal-loading').classList.remove('hidden')\n this.container.querySelector('.dcp-modal-body').classList.add('hidden')\n this.container.querySelector('.dcp-modal-footer').classList.add('hidden')\n }\n\n open () {\n this.form.addEventListener('submit', async (event) => {\n const success = await this.formSubmitHandler(event)\n if (success === false) {\n return\n }\n this.close(true)\n })\n // When the user clicks on <span> (x), close the modal\n this.closeButton.addEventListener('click', this.close.bind(this))\n this.cancelButton.addEventListener('click', this.close.bind(this))\n\n // Prevent lingering outlines after clicking some form elements.\n this.container.querySelectorAll('.dcp-modal-body button, .dcp-modal-body input[type=\"checkbox\"]').forEach(element => {\n element.addEventListener('click', () => {\n element.blur()\n })\n })\n\n // Show the modal.\n this.container.style.display = 'block'\n\n const formElements = this.container.querySelectorAll('.dcp-modal-body select, .dcp-modal-body input')\n if (formElements.length) {\n formElements[0].focus()\n if (formElements[0].type === 'text') {\n formElements[0].select()\n }\n for (const el of formElements) {\n if (el.realType) {\n el.type = el.realType\n }\n }\n } else {\n // With no form elements to allow for form submission on enter, focus the\n // continue button.\n this.container.querySelector('.dcp-modal-footer button.continue').focus()\n }\n } // TODO: This should return a promise with the action resolving it\n\n /**\n * Shows the modal and returns a promise of the result of the modal (e.g. was\n * it closed, did its action succeed?)\n */\n showModal () {\n return new Promise((resolve, reject) => {\n this.form.addEventListener('submit', handleContinue.bind(this))\n this.cancelButton.addEventListener('click', handleCancel.bind(this))\n this.closeButton.addEventListener('click', handleCancel.bind(this))\n\n // Prevent lingering outlines after clicking some form elements.\n this.container.querySelectorAll('.dcp-modal-body button, .dcp-modal-body input[type=\"checkbox\"]').forEach(element => {\n element.addEventListener('click', () => {\n element.blur()\n })\n })\n\n // Show the modal.\n this.container.style.display = 'block'\n\n const formElements = this.container.querySelectorAll('.dcp-modal-body select, .dcp-modal-body input')\n if (formElements.length) {\n formElements[0].focus()\n if (formElements[0].type === 'text') {\n formElements[0].select()\n }\n for (const el of formElements) {\n if (el.realType) {\n el.type = el.realType\n }\n }\n } else {\n // With no form elements to allow for form submission on enter, focus the\n // continue button.\n this.continueButton.focus()\n }\n\n async function handleContinue (event) {\n let result\n try {\n result = await this.formSubmitHandler(event)\n } catch (error) {\n reject(error)\n }\n this.close(true)\n resolve(result)\n }\n\n async function handleCancel () {\n let result\n try {\n result = await this.close()\n } catch (error) {\n reject(error)\n }\n resolve(result)\n }\n })\n }\n\n close (success = false) {\n this.container.style.display = 'none'\n if (this.container.parentNode) {\n this.container.parentNode.removeChild(this.container)\n }\n\n // @todo this needs to remove eventlisteners to prevent memory leaks\n\n if ((success !== true) && typeof this.exitHandler === 'function') {\n return this.exitHandler(this)\n }\n }\n\n /**\n * Adds different form elements to the modal depending on the case.\n *\n * @param {*} elements - The properties of the form elements to add.\n * @returns {HTMLElement} The input form elements.\n */\n addFormElement (...elements) {\n const body = this.container.querySelector('.dcp-modal-body')\n const inputElements = []\n let label\n for (let i = 0; i < elements.length; i++) {\n let row = document.createElement('div')\n row.className = 'row'\n\n let col, input\n switch (elements[i].type) {\n case 'button':\n col = document.createElement('div')\n col.className = 'col-md-12'\n\n input = document.createElement('button')\n input.innerHTML = elements[i].label\n input.type = 'button'\n input.classList.add('green-modal-button')\n if (!elements[i].onclick) {\n throw new Error('A button in the modal body should have an on click event handler.')\n }\n input.addEventListener('click', elements[i].onclick)\n\n col.appendChild(input)\n row.appendChild(col)\n break\n case 'textarea':\n col = document.createElement('div')\n col.className = 'col-md-12'\n\n input = document.createElement('textarea')\n input.className = 'text-input-field form-control'\n if (elements[i].placeholder) input.placeholder = elements[i].placeholder\n\n col.appendChild(input)\n row.appendChild(col)\n break\n case 'text':\n case 'email':\n case 'number':\n case 'password': {\n const inputCol = document.createElement('div')\n\n input = document.createElement('input')\n input.type = elements[i].type\n input.validation = elements[i].validation\n input.autocomplete = elements[i].autocomplete || (elements[i].type === 'password' ? 'off' : 'on')\n input.className = 'text-input-field form-control'\n\n // Adding bootstraps custom feedback styles.\n let invalidFeedback = null\n if (elements[i].invalidFeedback) {\n invalidFeedback = document.createElement('div')\n invalidFeedback.className = 'invalid-feedback'\n invalidFeedback.innerText = elements[i].invalidFeedback\n }\n\n if (elements[i].type === 'password') {\n elements[i].realType = 'password'\n }\n\n if (elements[i].label) {\n const labelCol = document.createElement('div')\n label = document.createElement('label')\n label.innerText = elements[i].label\n const inputId = 'dcp-modal-input-' + this.container.querySelectorAll('input[type=\"text\"], input[type=\"email\"], input[type=\"number\"], input[type=\"password\"]').length\n label.setAttribute('for', inputId)\n input.id = inputId\n labelCol.classList.add('col-md-6', 'label-column')\n labelCol.appendChild(label)\n row.appendChild(labelCol)\n inputCol.className = 'col-md-6'\n } else {\n inputCol.className = 'col-md-12'\n }\n\n inputCol.appendChild(input)\n if (invalidFeedback !== null) {\n inputCol.appendChild(invalidFeedback)\n }\n row.appendChild(inputCol)\n break\n }\n case 'select':\n col = document.createElement('div')\n col.className = 'col-md-4'\n\n label = document.createElement('span')\n label.innerText = elements[i].label\n\n col.appendChild(label)\n row.appendChild(col)\n\n col = document.createElement('div')\n col.className = 'col-md-8'\n\n input = document.createElement('select')\n\n col.appendChild(input)\n row.appendChild(col)\n break\n case 'checkbox': {\n row.classList.add('checkbox-row')\n const checkboxLabelCol = document.createElement('div')\n checkboxLabelCol.classList.add('label-column', 'checkbox-label-column')\n\n label = document.createElement('label')\n label.innerText = elements[i].label\n label.for = 'dcp-checkbox-input-' + this.container.querySelectorAll('input[type=\"checkbox\"]').length\n label.setAttribute('for', label.for)\n label.className = 'checkbox-label'\n\n checkboxLabelCol.appendChild(label)\n\n const checkboxCol = document.createElement('div')\n checkboxCol.classList.add('checkbox-column')\n\n input = document.createElement('input')\n input.type = 'checkbox'\n input.id = label.for\n if (elements[i].checked) {\n input.checked = true\n }\n\n checkboxCol.appendChild(input)\n\n if (elements[i].labelToTheRightOfCheckbox) {\n checkboxCol.classList.add('col-md-5')\n row.appendChild(checkboxCol)\n checkboxLabelCol.classList.add('col-md-7')\n row.appendChild(checkboxLabelCol)\n } else {\n checkboxLabelCol.classList.add('col-md-6')\n checkboxCol.classList.add('col-md-6')\n row.appendChild(checkboxLabelCol)\n row.appendChild(checkboxCol)\n }\n break\n }\n case 'file':\n [input, row] = this.addFileInput(elements[i], input, row)\n break\n case 'label':\n row.classList.add('label-row')\n label = document.createElement('label')\n label.innerText = elements[i].label\n row.appendChild(label)\n break\n }\n\n // Copy other possibly specified element properties:\n const inputPropertyNames = ['title', 'inputmode', 'value', 'minLength', 'maxLength', 'size', 'required', 'pattern', 'min', 'max', 'step', 'placeholder', 'accept', 'multiple', 'id', 'onkeypress', 'oninput', 'for', 'readonly', 'autocomplete']\n for (const propertyName of inputPropertyNames) {\n if (Object.prototype.hasOwnProperty.call(elements[i], propertyName)) {\n if (propertyName === 'for' && !label.hasAttribute(propertyName)) {\n label.setAttribute(propertyName, elements[i][propertyName])\n }\n if (propertyName.startsWith('on')) {\n input.addEventListener(propertyName.slice(2), elements[i][propertyName])\n } else {\n input.setAttribute(propertyName, elements[i][propertyName])\n }\n }\n }\n\n inputElements.push(input)\n body.appendChild(row)\n }\n\n if (inputElements.length === 1) return inputElements[0]\n else return inputElements\n }\n\n /**\n * Adds a drag and drop file form element to the modal.\n *\n * @param {*} fileInputProperties - An object specifying some of the\n * properties of the file input element.\n * @param {*} fileInput - Placeholders to help create the file\n * input.\n * @param {HTMLDivElement} row - Placeholders to help create the file\n * input.\n */\n addFileInput (fileInputProperties, fileInput, row) {\n // Adding the upload label.\n const uploadLabel = document.createElement('label')\n uploadLabel.innerText = fileInputProperties.label\n row.appendChild(uploadLabel)\n const body = this.container.querySelector('.dcp-modal-body')\n body.appendChild(row)\n const fileSelectionRow = document.createElement('div')\n fileSelectionRow.id = 'file-selection-row'\n\n // Adding the drag and drop file upload input.\n const dropContainer = document.createElement('div')\n dropContainer.id = 'drop-container'\n\n // Adding an image of a wallet\n const imageContainer = document.createElement('div')\n imageContainer.id = 'image-container'\n const walletImage = document.createElement('span')\n walletImage.classList.add('fas', 'fa-wallet')\n imageContainer.appendChild(walletImage)\n\n // Adding some text prompts\n const dropMessage = document.createElement('span')\n dropMessage.innerText = 'Drop a keystore file here'\n const orMessage = document.createElement('span')\n orMessage.innerText = 'or'\n\n // Adding the manual file input element (hiding the default one)\n const fileInputContainer = document.createElement('div')\n const fileInputLabel = document.createElement('label')\n // Linking the label to the file input so that clicking on the label\n // activates the file input.\n fileInputLabel.setAttribute('for', 'file-input')\n fileInputLabel.innerText = 'Browse'\n fileInput = document.createElement('input')\n fileInput.type = fileInputProperties.type\n fileInput.id = 'file-input'\n // To remove the lingering outline after selecting the file.\n fileInput.addEventListener('click', () => {\n fileInput.blur()\n })\n fileInputContainer.append(fileInput, fileInputLabel)\n\n // Creating the final row element to append to the modal body.\n dropContainer.append(imageContainer, dropMessage, orMessage, fileInputContainer)\n fileSelectionRow.appendChild(dropContainer)\n\n // Adding functionality to the drag and drop file input.\n dropContainer.addEventListener('drop', selectDroppedFile.bind(this))\n dropContainer.addEventListener('drop', unhighlightDropArea)\n // Prevent file from being opened by the browser.\n dropContainer.ondragover = highlightDropArea\n dropContainer.ondragenter = highlightDropArea\n dropContainer.ondragleave = unhighlightDropArea\n\n fileInput.addEventListener('change', handleFileChange)\n\n const fileNamePlaceholder = document.createElement('center')\n fileNamePlaceholder.id = 'file-name-placeholder'\n fileNamePlaceholder.className = 'row'\n fileNamePlaceholder.innerText = ''\n fileSelectionRow.appendChild(fileNamePlaceholder)\n fileNamePlaceholder.classList.add('hidden')\n\n // Check if the continue button is invalid on the keystore upload modal and\n // click it if it should no longer be invalid.\n this.continueButton.addEventListener('invalid', () => {\n const fileFormElements = this.container.querySelectorAll('.dcp-modal-body input[type=\"file\"], .dcp-modal-body input[type=\"text\"]')\n const filledInFileFormElements = Array.from(fileFormElements).filter(fileFormElement => fileFormElement.value !== '')\n if (fileFormElements.length !== 0 && filledInFileFormElements.length !== 0) {\n this.continueButton.setCustomValidity('')\n // Clicking instead of dispatching a submit event to ensure other form validation is used before submitting the form.\n this.continueButton.click()\n }\n })\n\n return [fileInput, fileSelectionRow]\n\n /**\n * Checks that the dropped items contain only a single keystore file.\n * If valid, sets the file input's value to the dropped file.\n * @param {DragEvent} event - Contains the files dropped.\n */\n function selectDroppedFile (event) {\n // Prevent file from being opened.\n event.preventDefault()\n\n // Check if only one file was dropped.\n const wasOneFileDropped = event.dataTransfer.items.length === 1 ||\n event.dataTransfer.files.length === 1\n updateFileSelectionStatus(wasOneFileDropped)\n if (!wasOneFileDropped) {\n fileInput.setCustomValidity('Only one file can be uploaded.')\n fileInput.reportValidity()\n return\n } else {\n fileInput.setCustomValidity('')\n }\n\n // Now to use the DataTransfer interface to access the file(s), setting\n // the value of the file input.\n const file = event.dataTransfer.files[0]\n\n if (checkFileExtension(file)) {\n fileInput.files = event.dataTransfer.files\n fileInput.dispatchEvent(new Event('change'))\n }\n }\n\n function handleFileChange () {\n if (checkFileExtension(this.files[0]) && this.files.length === 1) {\n fileNamePlaceholder.innerText = `Selected File: ${this.files[0].name}`\n updateFileSelectionStatus(true)\n // Invoke a callback if additional functionality is required.\n if (typeof fileInputProperties.callback === 'function') {\n fileInputProperties.callback(this.files[0])\n }\n }\n }\n\n /**\n * Checks if the file extension on the inputted file is correct.\n * @param {File} file - The file to check\n * @returns {boolean} True if the file extension is valid, false otherwise.\n */\n function checkFileExtension (file) {\n // If there's no restriction, return true.\n if (!fileInputProperties.extension) {\n return true\n }\n const fileExtension = file.name.split('.').pop()\n const isValidExtension = fileExtension === fileInputProperties.extension\n updateFileSelectionStatus(isValidExtension)\n if (!isValidExtension) {\n fileInput.setCustomValidity(`Only a .${fileInputProperties.extension} file can be uploaded.`)\n fileInput.reportValidity()\n fileNamePlaceholder.classList.add('hidden')\n } else {\n fileInput.setCustomValidity('')\n }\n return isValidExtension\n }\n\n /**\n * Updates the file input to reflect the validity of the current file\n * selection.\n * @param {boolean} isValidFileSelection - True if a single .keystore file\n * was selected. False otherwise.\n */\n function updateFileSelectionStatus (isValidFileSelection) {\n imageContainer.innerHTML = ''\n const statusImage = document.createElement('span')\n statusImage.classList.add('fas', isValidFileSelection ? 'fa-check' : 'fa-times')\n statusImage.style.color = isValidFileSelection ? 'green' : 'red'\n imageContainer.appendChild(statusImage)\n\n if (!isValidFileSelection) {\n fileInput.value = null\n fileNamePlaceholder.classList.add('hidden')\n } else {\n fileNamePlaceholder.classList.remove('hidden')\n }\n\n // If the modal contains a password field for a keystore file, change its\n // visibility.\n const walletPasswordInputContainer = document.querySelector('.dcp-modal-body input[type=\"password\"]').parentElement.parentElement\n if (walletPasswordInputContainer) {\n if (isValidFileSelection) {\n walletPasswordInputContainer.classList.remove('hidden')\n const walletPasswordInput = document.querySelector('.dcp-modal-body input[type=\"password\"]')\n walletPasswordInput.focus()\n } else {\n walletPasswordInputContainer.classList.add('hidden')\n }\n }\n }\n\n function highlightDropArea (event) {\n event.preventDefault()\n this.classList.add('highlight')\n }\n\n function unhighlightDropArea (event) {\n event.preventDefault()\n this.classList.remove('highlight')\n }\n }\n\n /**\n * Sets up a custom tooltip to pop up when the passwords do not match, but are\n * valid otherwise.\n */\n addFormValidationForPasswordConfirmation () {\n const [newPassword, confirmPassword] = document.querySelectorAll('.dcp-modal-body input[type=\"password\"]')\n if (!newPassword || !confirmPassword) {\n throw Error('New Password field and Confirm Password fields not present.')\n }\n\n newPassword.addEventListener('input', checkMatchingPasswords)\n confirmPassword.addEventListener('input', checkMatchingPasswords)\n\n function checkMatchingPasswords () {\n if (newPassword.value !== confirmPassword.value &&\n newPassword.validity.valid &&\n confirmPassword.validity.valid) {\n newPassword.setCustomValidity('Both passwords must match.')\n } else if (newPassword.value === confirmPassword.value ||\n newPassword.validity.tooShort ||\n newPassword.validity.patternMismatch ||\n newPassword.validity.valueMissing ||\n confirmPassword.validity.tooShort ||\n confirmPassword.validity.patternMismatch ||\n confirmPassword.validity.valueMissing) {\n // If the passwords fields match or have become invalidated some other\n // way again, reset the custom message.\n newPassword.setCustomValidity('')\n }\n }\n }\n\n updateInvalidEmailMessage() {\n const email = document.querySelector('.dcp-modal-body input[id=\"email\"')\n if (!email){\n throw Error(\"Email field not present\")\n }\n email.addEventListener('input', checkValidEmail);\n function checkValidEmail() {\n if (!email.validity.patternMismatch &&\n !email.validity.valueMissing) {\n email.setCustomValidity('')\n } else {\n email.setCustomValidity(\"Enter a valid email address.\")\n }\n\n }\n }\n\n /**\n * Adds message(s) to the modal's body.\n * @param {string} messages - The message(s) to add to the modal's body.\n * @returns Paragraph element(s) containing the message(s) added to the\n * modal's body.\n */\n addMessage (...messages) {\n const elements = []\n const body = this.container.querySelector('.dcp-modal-body')\n for (let i = 0; i < messages.length; i++) {\n const row = document.createElement('div')\n row.className = 'row'\n\n const paragraph = document.createElement('p')\n paragraph.innerHTML = messages[i]\n paragraph.classList.add('message')\n row.appendChild(paragraph)\n body.appendChild(row)\n\n elements.push(paragraph)\n }\n\n if (elements.length === 1) return elements[0]\n else return elements\n }\n\n addHorizontalRule () {\n const body = this.container.querySelector('.dcp-modal-body')\n body.appendChild(document.createElement('hr'))\n }\n\n // Does what it says. Still ill advised to use unless you have to.\n addCustomHTML (htmlStr, browseCallback) {\n const elements = []\n const body = this.container.querySelector('.dcp-modal-body')\n body.innerHTML += htmlStr\n body.querySelector('#browse-button').addEventListener('click', browseCallback.bind(this, this))\n\n if (elements.length === 1) return elements[0]\n else return elements\n }\n\n addButton (...buttons) {\n const elements = []\n const body = this.container.querySelector('.dcp-modal-body')\n for (let i = 0; i < buttons.length; i++) {\n const row = document.createElement('div')\n row.className = 'row'\n\n let col = document.createElement('div')\n col.className = 'col-md-4'\n\n const description = document.createElement('span')\n description.innerText = buttons[i].description\n\n col.appendChild(description)\n row.appendChild(col)\n\n col = document.createElement('div')\n col.className = 'col-md-8'\n\n const button = document.createElement('button')\n button.innerText = buttons[i].label\n button.addEventListener('click', buttons[i].callback.bind(this, this))\n\n elements.push(button)\n\n col.appendChild(button)\n row.appendChild(col)\n\n body.appendChild(row)\n }\n\n if (elements.length === 1) return elements[0]\n else return elements\n }\n}\n\n\n// Inject our special stylesheet from dcp-client only if we're on the portal webpage.\nif (typeof window !== 'undefined' && typeof document !== 'undefined' && dcpConfig.portal.location.hostname === window.location.hostname) {\n // <link rel='stylesheet' href='/css/dashboard.css'>\n const stylesheet = document.createElement('link')\n stylesheet.rel = 'stylesheet'\n // Needed for the duplicate check done later.\n stylesheet.id = 'dcp-modal-styles'\n\n const dcpClientBundle = document.getElementById('_dcp_client_bundle')\n let src\n if (dcpClientBundle) {\n src = dcpClientBundle.src.replace('dcp-client-bundle.js', 'dcp-modal-style.css')\n } else {\n src = dcpConfig.portal.location.href + 'dcp-client/dist/dcp-modal-style.css'\n }\n\n stylesheet.href = src\n // If the style was injected before, don't inject it again.\n // Could occur when loading a file that imports Modal.js and loading\n // comput.min.js in the same HTML file.\n if (document.getElementById(stylesheet.id) === null) {\n document.getElementsByTagName('head')[0].appendChild(stylesheet)\n }\n\n if (typeof {\"version\":\"4dd484422936278679ed70ce7501d11964b9d792\",\"branch\":\"release\",\"dcpClient\":{\"version\":\"4.2.11\",\"from\":\"git+ssh://git@gitlab.com/Distributed-Compute-Protocol/dcp-client.git#prod-20220815\",\"resolved\":\"git+ssh://git@gitlab.com/Distributed-Compute-Protocol/dcp-client.git#abc5acb71dc4f13fc06a196f98e3c58f01bed960\"},\"built\":\"Thu Aug 18 2022 09:29:50 GMT-0400 (Eastern Daylight Saving Time)\",\"config\":{\"generated\":\"Thu 18 Aug 2022 09:29:47 AM EDT by erose on lorge\",\"build\":\"debug\"},\"webpack\":\"5.70.0\",\"node\":\"v14.20.0\"} !== 'undefined' && typeof window.Modal === 'undefined') {\n window.Modal = Modal\n }\n}\n\n\n//# sourceURL=webpack://dcp/./portal/www/js/modal.js?");
3846
+ ||||||| e0e85dd
3847
+ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": () => (/* binding */ Modal)\n/* harmony export */ });\n/**\n * A Small Modal Class\n * @module Modal\n */\n/* globals Event dcpConfig */\nclass Modal {\n constructor (title, message, callback = false, exitHandler = false, {\n continueLabel = 'Continue',\n cancelLabel = 'Cancel',\n cancelVisible = true\n } = {}) {\n const modal = document.createElement('div')\n modal.className = 'dcp-modal-container-old day'\n modal.innerHTML = `\n <dialog class=\"dcp-modal-content\">\n <div class=\"dcp-modal-header\">\n <h2>${title}<button type=\"button\" class=\"close\">&times;</button></h2>\n ${message ? '<p>' + message + '</p>' : ''}\n </div>\n <div class=\"dcp-modal-loading hidden\">\n <div class='loading'></div>\n </div>\n <form onsubmit='return false' method=\"dialog\">\n <div class=\"dcp-modal-body\"></div>\n <div class=\"dcp-modal-footer ${cancelVisible ? '' : 'centered'}\">\n <button type=\"submit\" class=\"continue green-modal-button\">${continueLabel}</button>\n <button type=\"button\" class=\"cancel green-modal-button\">${cancelLabel}</button>\n </div>\n </form>\n </dialog>`\n\n // To give a reference to do developer who wants to override the form submit.\n // May occur if they want to validate the information in the backend\n // without closing the modal prematurely.\n this.form = modal.querySelector('.dcp-modal-content form')\n this.continueButton = modal.querySelector('.dcp-modal-footer button.continue')\n this.cancelButton = modal.querySelector('.dcp-modal-footer button.cancel')\n this.closeButton = modal.querySelector('.dcp-modal-header .close')\n if (!cancelVisible) {\n this.cancelButton.style.display = 'none'\n }\n\n // To remove the event listener, the reference to the original function\n // added is required.\n this.formSubmitHandler = this.continue.bind(this)\n\n modal.addEventListener('keydown', function (event) {\n event.stopPropagation()\n // 27 is the keycode for the escape key.\n if (event.keyCode === 27) this.close()\n }.bind(this))\n\n this.container = modal\n this.callback = callback\n this.exitHandler = exitHandler\n document.body.appendChild(modal)\n }\n\n changeFormSubmitHandler (newFormSubmitHandler) {\n this.formSubmitHandler = newFormSubmitHandler\n }\n\n /**\n * Validates the form values in the modal and calls the modal's callback\n */\n async continue (event) {\n // To further prevent form submission from trying to redirect from the\n // current page.\n if (event instanceof Event) {\n event.preventDefault()\n }\n let fieldsAreValid = true\n let formElements = this.container.querySelectorAll('.dcp-modal-body select, .dcp-modal-body input, .dcp-modal-body textarea')\n\n const formValues = []\n if (typeof formElements.length === 'undefined') formElements = [formElements]\n // Separate into two loops to enable input validation requiring formValues\n // that come after it. e.g. Two password fields matching.\n for (let i = 0; i < formElements.length; i++) {\n switch (formElements[i].type) {\n case 'file':\n formValues.push(formElements[i])\n break\n case 'checkbox':\n formValues.push(formElements[i].checked)\n break\n default:\n formValues.push(formElements[i].value)\n break\n }\n }\n for (let i = 0; i < formElements.length; i++) {\n if (formElements[i].validation) {\n // Optional fields are allowed to be empty but still can't be wrong if not empty.\n if (!(formElements[i].value === '' && !formElements[i].required)) {\n if (typeof formElements[i].validation === 'function') {\n if (!formElements[i].validation(formValues)) {\n fieldsAreValid = false\n formElements[i].classList.add('is-invalid')\n }\n } else if (!formElements[i].validation.test(formElements[i].value)) {\n fieldsAreValid = false\n formElements[i].classList.add('is-invalid')\n }\n }\n }\n }\n\n if (!fieldsAreValid) return\n\n this.loading()\n if (typeof this.callback === 'function') {\n try {\n return this.callback(formValues)\n } catch (error) {\n console.error('Unexpected error in modal.continue:', error);\n return this.close(false)\n }\n }\n this.close(true)\n }\n\n loading () {\n this.container.querySelector('.dcp-modal-loading').classList.remove('hidden')\n this.container.querySelector('.dcp-modal-body').classList.add('hidden')\n this.container.querySelector('.dcp-modal-footer').classList.add('hidden')\n }\n\n open () {\n this.form.addEventListener('submit', async (event) => {\n const success = await this.formSubmitHandler(event)\n if (success === false) {\n return\n }\n this.close(true)\n })\n // When the user clicks on <span> (x), close the modal\n this.closeButton.addEventListener('click', this.close.bind(this))\n this.cancelButton.addEventListener('click', this.close.bind(this))\n\n // Prevent lingering outlines after clicking some form elements.\n this.container.querySelectorAll('.dcp-modal-body button, .dcp-modal-body input[type=\"checkbox\"]').forEach(element => {\n element.addEventListener('click', () => {\n element.blur()\n })\n })\n\n // Show the modal.\n this.container.style.display = 'block'\n\n const formElements = this.container.querySelectorAll('.dcp-modal-body select, .dcp-modal-body input')\n if (formElements.length) {\n formElements[0].focus()\n if (formElements[0].type === 'text') {\n formElements[0].select()\n }\n for (const el of formElements) {\n if (el.realType) {\n el.type = el.realType\n }\n }\n } else {\n // With no form elements to allow for form submission on enter, focus the\n // continue button.\n this.container.querySelector('.dcp-modal-footer button.continue').focus()\n }\n } // TODO: This should return a promise with the action resolving it\n\n /**\n * Shows the modal and returns a promise of the result of the modal (e.g. was\n * it closed, did its action succeed?)\n */\n showModal () {\n return new Promise((resolve, reject) => {\n this.form.addEventListener('submit', handleContinue.bind(this))\n this.cancelButton.addEventListener('click', handleCancel.bind(this))\n this.closeButton.addEventListener('click', handleCancel.bind(this))\n\n // Prevent lingering outlines after clicking some form elements.\n this.container.querySelectorAll('.dcp-modal-body button, .dcp-modal-body input[type=\"checkbox\"]').forEach(element => {\n element.addEventListener('click', () => {\n element.blur()\n })\n })\n\n // Show the modal.\n this.container.style.display = 'block'\n\n const formElements = this.container.querySelectorAll('.dcp-modal-body select, .dcp-modal-body input')\n if (formElements.length) {\n formElements[0].focus()\n if (formElements[0].type === 'text') {\n formElements[0].select()\n }\n for (const el of formElements) {\n if (el.realType) {\n el.type = el.realType\n }\n }\n } else {\n // With no form elements to allow for form submission on enter, focus the\n // continue button.\n this.continueButton.focus()\n }\n\n async function handleContinue (event) {\n let result\n try {\n result = await this.formSubmitHandler(event)\n } catch (error) {\n reject(error)\n }\n this.close(true)\n resolve(result)\n }\n\n async function handleCancel () {\n let result\n try {\n result = await this.close()\n } catch (error) {\n reject(error)\n }\n resolve(result)\n }\n })\n }\n\n close (success = false) {\n this.container.style.display = 'none'\n if (this.container.parentNode) {\n this.container.parentNode.removeChild(this.container)\n }\n\n // @todo this needs to remove eventlisteners to prevent memory leaks\n\n if ((success !== true) && typeof this.exitHandler === 'function') {\n return this.exitHandler(this)\n }\n }\n\n /**\n * Adds different form elements to the modal depending on the case.\n *\n * @param {*} elements - The properties of the form elements to add.\n * @returns {HTMLElement} The input form elements.\n */\n addFormElement (...elements) {\n const body = this.container.querySelector('.dcp-modal-body')\n const inputElements = []\n let label\n for (let i = 0; i < elements.length; i++) {\n let row = document.createElement('div')\n row.className = 'row'\n\n let col, input\n switch (elements[i].type) {\n case 'button':\n col = document.createElement('div')\n col.className = 'col-md-12'\n\n input = document.createElement('button')\n input.innerHTML = elements[i].label\n input.type = 'button'\n input.classList.add('green-modal-button')\n if (!elements[i].onclick) {\n throw new Error('A button in the modal body should have an on click event handler.')\n }\n input.addEventListener('click', elements[i].onclick)\n\n col.appendChild(input)\n row.appendChild(col)\n break\n case 'textarea':\n col = document.createElement('div')\n col.className = 'col-md-12'\n\n input = document.createElement('textarea')\n input.className = 'text-input-field form-control'\n if (elements[i].placeholder) input.placeholder = elements[i].placeholder\n\n col.appendChild(input)\n row.appendChild(col)\n break\n case 'text':\n case 'email':\n case 'number':\n case 'password': {\n const inputCol = document.createElement('div')\n\n input = document.createElement('input')\n input.type = elements[i].type\n input.validation = elements[i].validation\n input.autocomplete = elements[i].autocomplete || (elements[i].type === 'password' ? 'off' : 'on')\n input.className = 'text-input-field form-control'\n\n // Adding bootstraps custom feedback styles.\n let invalidFeedback = null\n if (elements[i].invalidFeedback) {\n invalidFeedback = document.createElement('div')\n invalidFeedback.className = 'invalid-feedback'\n invalidFeedback.innerText = elements[i].invalidFeedback\n }\n\n if (elements[i].type === 'password') {\n elements[i].realType = 'password'\n }\n\n if (elements[i].label) {\n const labelCol = document.createElement('div')\n label = document.createElement('label')\n label.innerText = elements[i].label\n const inputId = 'dcp-modal-input-' + this.container.querySelectorAll('input[type=\"text\"], input[type=\"email\"], input[type=\"number\"], input[type=\"password\"]').length\n label.setAttribute('for', inputId)\n input.id = inputId\n labelCol.classList.add('col-md-6', 'label-column')\n labelCol.appendChild(label)\n row.appendChild(labelCol)\n inputCol.className = 'col-md-6'\n } else {\n inputCol.className = 'col-md-12'\n }\n\n inputCol.appendChild(input)\n if (invalidFeedback !== null) {\n inputCol.appendChild(invalidFeedback)\n }\n row.appendChild(inputCol)\n break\n }\n case 'select':\n col = document.createElement('div')\n col.className = 'col-md-4'\n\n label = document.createElement('span')\n label.innerText = elements[i].label\n\n col.appendChild(label)\n row.appendChild(col)\n\n col = document.createElement('div')\n col.className = 'col-md-8'\n\n input = document.createElement('select')\n\n col.appendChild(input)\n row.appendChild(col)\n break\n case 'checkbox': {\n row.classList.add('checkbox-row')\n const checkboxLabelCol = document.createElement('div')\n checkboxLabelCol.classList.add('label-column', 'checkbox-label-column')\n\n label = document.createElement('label')\n label.innerText = elements[i].label\n label.for = 'dcp-checkbox-input-' + this.container.querySelectorAll('input[type=\"checkbox\"]').length\n label.setAttribute('for', label.for)\n label.className = 'checkbox-label'\n\n checkboxLabelCol.appendChild(label)\n\n const checkboxCol = document.createElement('div')\n checkboxCol.classList.add('checkbox-column')\n\n input = document.createElement('input')\n input.type = 'checkbox'\n input.id = label.for\n if (elements[i].checked) {\n input.checked = true\n }\n\n checkboxCol.appendChild(input)\n\n if (elements[i].labelToTheRightOfCheckbox) {\n checkboxCol.classList.add('col-md-5')\n row.appendChild(checkboxCol)\n checkboxLabelCol.classList.add('col-md-7')\n row.appendChild(checkboxLabelCol)\n } else {\n checkboxLabelCol.classList.add('col-md-6')\n checkboxCol.classList.add('col-md-6')\n row.appendChild(checkboxLabelCol)\n row.appendChild(checkboxCol)\n }\n break\n }\n case 'file':\n [input, row] = this.addFileInput(elements[i], input, row)\n break\n case 'label':\n row.classList.add('label-row')\n label = document.createElement('label')\n label.innerText = elements[i].label\n row.appendChild(label)\n break\n }\n\n // Copy other possibly specified element properties:\n const inputPropertyNames = ['title', 'inputmode', 'value', 'minLength', 'maxLength', 'size', 'required', 'pattern', 'min', 'max', 'step', 'placeholder', 'accept', 'multiple', 'id', 'onkeypress', 'oninput', 'for', 'readonly', 'autocomplete']\n for (const propertyName of inputPropertyNames) {\n if (Object.prototype.hasOwnProperty.call(elements[i], propertyName)) {\n if (propertyName === 'for' && !label.hasAttribute(propertyName)) {\n label.setAttribute(propertyName, elements[i][propertyName])\n }\n if (propertyName.startsWith('on')) {\n input.addEventListener(propertyName.slice(2), elements[i][propertyName])\n } else {\n input.setAttribute(propertyName, elements[i][propertyName])\n }\n }\n }\n\n inputElements.push(input)\n body.appendChild(row)\n }\n\n if (inputElements.length === 1) return inputElements[0]\n else return inputElements\n }\n\n /**\n * Adds a drag and drop file form element to the modal.\n *\n * @param {*} fileInputProperties - An object specifying some of the\n * properties of the file input element.\n * @param {*} fileInput - Placeholders to help create the file\n * input.\n * @param {HTMLDivElement} row - Placeholders to help create the file\n * input.\n */\n addFileInput (fileInputProperties, fileInput, row) {\n // Adding the upload label.\n const uploadLabel = document.createElement('label')\n uploadLabel.innerText = fileInputProperties.label\n row.appendChild(uploadLabel)\n const body = this.container.querySelector('.dcp-modal-body')\n body.appendChild(row)\n const fileSelectionRow = document.createElement('div')\n fileSelectionRow.id = 'file-selection-row'\n\n // Adding the drag and drop file upload input.\n const dropContainer = document.createElement('div')\n dropContainer.id = 'drop-container'\n\n // Adding an image of a wallet\n const imageContainer = document.createElement('div')\n imageContainer.id = 'image-container'\n const walletImage = document.createElement('span')\n walletImage.classList.add('fas', 'fa-wallet')\n imageContainer.appendChild(walletImage)\n\n // Adding some text prompts\n const dropMessage = document.createElement('span')\n dropMessage.innerText = 'Drop a keystore file here'\n const orMessage = document.createElement('span')\n orMessage.innerText = 'or'\n\n // Adding the manual file input element (hiding the default one)\n const fileInputContainer = document.createElement('div')\n const fileInputLabel = document.createElement('label')\n // Linking the label to the file input so that clicking on the label\n // activates the file input.\n fileInputLabel.setAttribute('for', 'file-input')\n fileInputLabel.innerText = 'Browse'\n fileInput = document.createElement('input')\n fileInput.type = fileInputProperties.type\n fileInput.id = 'file-input'\n // To remove the lingering outline after selecting the file.\n fileInput.addEventListener('click', () => {\n fileInput.blur()\n })\n fileInputContainer.append(fileInput, fileInputLabel)\n\n // Creating the final row element to append to the modal body.\n dropContainer.append(imageContainer, dropMessage, orMessage, fileInputContainer)\n fileSelectionRow.appendChild(dropContainer)\n\n // Adding functionality to the drag and drop file input.\n dropContainer.addEventListener('drop', selectDroppedFile.bind(this))\n dropContainer.addEventListener('drop', unhighlightDropArea)\n // Prevent file from being opened by the browser.\n dropContainer.ondragover = highlightDropArea\n dropContainer.ondragenter = highlightDropArea\n dropContainer.ondragleave = unhighlightDropArea\n\n fileInput.addEventListener('change', handleFileChange)\n\n const fileNamePlaceholder = document.createElement('center')\n fileNamePlaceholder.id = 'file-name-placeholder'\n fileNamePlaceholder.className = 'row'\n fileNamePlaceholder.innerText = ''\n fileSelectionRow.appendChild(fileNamePlaceholder)\n fileNamePlaceholder.classList.add('hidden')\n\n // Check if the continue button is invalid on the keystore upload modal and\n // click it if it should no longer be invalid.\n this.continueButton.addEventListener('invalid', () => {\n const fileFormElements = this.container.querySelectorAll('.dcp-modal-body input[type=\"file\"], .dcp-modal-body input[type=\"text\"]')\n const filledInFileFormElements = Array.from(fileFormElements).filter(fileFormElement => fileFormElement.value !== '')\n if (fileFormElements.length !== 0 && filledInFileFormElements.length !== 0) {\n this.continueButton.setCustomValidity('')\n // Clicking instead of dispatching a submit event to ensure other form validation is used before submitting the form.\n this.continueButton.click()\n }\n })\n\n return [fileInput, fileSelectionRow]\n\n /**\n * Checks that the dropped items contain only a single keystore file.\n * If valid, sets the file input's value to the dropped file.\n * @param {DragEvent} event - Contains the files dropped.\n */\n function selectDroppedFile (event) {\n // Prevent file from being opened.\n event.preventDefault()\n\n // Check if only one file was dropped.\n const wasOneFileDropped = event.dataTransfer.items.length === 1 ||\n event.dataTransfer.files.length === 1\n updateFileSelectionStatus(wasOneFileDropped)\n if (!wasOneFileDropped) {\n fileInput.setCustomValidity('Only one file can be uploaded.')\n fileInput.reportValidity()\n return\n } else {\n fileInput.setCustomValidity('')\n }\n\n // Now to use the DataTransfer interface to access the file(s), setting\n // the value of the file input.\n const file = event.dataTransfer.files[0]\n\n if (checkFileExtension(file)) {\n fileInput.files = event.dataTransfer.files\n fileInput.dispatchEvent(new Event('change'))\n }\n }\n\n function handleFileChange () {\n if (checkFileExtension(this.files[0]) && this.files.length === 1) {\n fileNamePlaceholder.innerText = `Selected File: ${this.files[0].name}`\n updateFileSelectionStatus(true)\n // Invoke a callback if additional functionality is required.\n if (typeof fileInputProperties.callback === 'function') {\n fileInputProperties.callback(this.files[0])\n }\n }\n }\n\n /**\n * Checks if the file extension on the inputted file is correct.\n * @param {File} file - The file to check\n * @returns {boolean} True if the file extension is valid, false otherwise.\n */\n function checkFileExtension (file) {\n // If there's no restriction, return true.\n if (!fileInputProperties.extension) {\n return true\n }\n const fileExtension = file.name.split('.').pop()\n const isValidExtension = fileExtension === fileInputProperties.extension\n updateFileSelectionStatus(isValidExtension)\n if (!isValidExtension) {\n fileInput.setCustomValidity(`Only a .${fileInputProperties.extension} file can be uploaded.`)\n fileInput.reportValidity()\n fileNamePlaceholder.classList.add('hidden')\n } else {\n fileInput.setCustomValidity('')\n }\n return isValidExtension\n }\n\n /**\n * Updates the file input to reflect the validity of the current file\n * selection.\n * @param {boolean} isValidFileSelection - True if a single .keystore file\n * was selected. False otherwise.\n */\n function updateFileSelectionStatus (isValidFileSelection) {\n imageContainer.innerHTML = ''\n const statusImage = document.createElement('span')\n statusImage.classList.add('fas', isValidFileSelection ? 'fa-check' : 'fa-times')\n statusImage.style.color = isValidFileSelection ? 'green' : 'red'\n imageContainer.appendChild(statusImage)\n\n if (!isValidFileSelection) {\n fileInput.value = null\n fileNamePlaceholder.classList.add('hidden')\n } else {\n fileNamePlaceholder.classList.remove('hidden')\n }\n\n // If the modal contains a password field for a keystore file, change its\n // visibility.\n const walletPasswordInputContainer = document.querySelector('.dcp-modal-body input[type=\"password\"]').parentElement.parentElement\n if (walletPasswordInputContainer) {\n if (isValidFileSelection) {\n walletPasswordInputContainer.classList.remove('hidden')\n const walletPasswordInput = document.querySelector('.dcp-modal-body input[type=\"password\"]')\n walletPasswordInput.focus()\n } else {\n walletPasswordInputContainer.classList.add('hidden')\n }\n }\n }\n\n function highlightDropArea (event) {\n event.preventDefault()\n this.classList.add('highlight')\n }\n\n function unhighlightDropArea (event) {\n event.preventDefault()\n this.classList.remove('highlight')\n }\n }\n\n /**\n * Sets up a custom tooltip to pop up when the passwords do not match, but are\n * valid otherwise.\n */\n addFormValidationForPasswordConfirmation () {\n const [newPassword, confirmPassword] = document.querySelectorAll('.dcp-modal-body input[type=\"password\"]')\n if (!newPassword || !confirmPassword) {\n throw Error('New Password field and Confirm Password fields not present.')\n }\n\n newPassword.addEventListener('input', checkMatchingPasswords)\n confirmPassword.addEventListener('input', checkMatchingPasswords)\n\n function checkMatchingPasswords () {\n if (newPassword.value !== confirmPassword.value &&\n newPassword.validity.valid &&\n confirmPassword.validity.valid) {\n newPassword.setCustomValidity('Both passwords must match.')\n } else if (newPassword.value === confirmPassword.value ||\n newPassword.validity.tooShort ||\n newPassword.validity.patternMismatch ||\n newPassword.validity.valueMissing ||\n confirmPassword.validity.tooShort ||\n confirmPassword.validity.patternMismatch ||\n confirmPassword.validity.valueMissing) {\n // If the passwords fields match or have become invalidated some other\n // way again, reset the custom message.\n newPassword.setCustomValidity('')\n }\n }\n }\n\n updateInvalidEmailMessage() {\n const email = document.querySelector('.dcp-modal-body input[id=\"email\"')\n if (!email){\n throw Error(\"Email field not present\")\n }\n email.addEventListener('input', checkValidEmail);\n function checkValidEmail() {\n if (!email.validity.patternMismatch &&\n !email.validity.valueMissing) {\n email.setCustomValidity('')\n } else {\n email.setCustomValidity(\"Enter a valid email address.\")\n }\n\n }\n }\n\n /**\n * Adds message(s) to the modal's body.\n * @param {string} messages - The message(s) to add to the modal's body.\n * @returns Paragraph element(s) containing the message(s) added to the\n * modal's body.\n */\n addMessage (...messages) {\n const elements = []\n const body = this.container.querySelector('.dcp-modal-body')\n for (let i = 0; i < messages.length; i++) {\n const row = document.createElement('div')\n row.className = 'row'\n\n const paragraph = document.createElement('p')\n paragraph.innerHTML = messages[i]\n paragraph.classList.add('message')\n row.appendChild(paragraph)\n body.appendChild(row)\n\n elements.push(paragraph)\n }\n\n if (elements.length === 1) return elements[0]\n else return elements\n }\n\n addHorizontalRule () {\n const body = this.container.querySelector('.dcp-modal-body')\n body.appendChild(document.createElement('hr'))\n }\n\n // Does what it says. Still ill advised to use unless you have to.\n addCustomHTML (htmlStr, browseCallback) {\n const elements = []\n const body = this.container.querySelector('.dcp-modal-body')\n body.innerHTML += htmlStr\n body.querySelector('#browse-button').addEventListener('click', browseCallback.bind(this, this))\n\n if (elements.length === 1) return elements[0]\n else return elements\n }\n\n addButton (...buttons) {\n const elements = []\n const body = this.container.querySelector('.dcp-modal-body')\n for (let i = 0; i < buttons.length; i++) {\n const row = document.createElement('div')\n row.className = 'row'\n\n let col = document.createElement('div')\n col.className = 'col-md-4'\n\n const description = document.createElement('span')\n description.innerText = buttons[i].description\n\n col.appendChild(description)\n row.appendChild(col)\n\n col = document.createElement('div')\n col.className = 'col-md-8'\n\n const button = document.createElement('button')\n button.innerText = buttons[i].label\n button.addEventListener('click', buttons[i].callback.bind(this, this))\n\n elements.push(button)\n\n col.appendChild(button)\n row.appendChild(col)\n\n body.appendChild(row)\n }\n\n if (elements.length === 1) return elements[0]\n else return elements\n }\n}\n\n\n// Inject our special stylesheet from dcp-client only if we're on the portal webpage.\nif (typeof window !== 'undefined' && typeof document !== 'undefined' && dcpConfig.portal.location.hostname === window.location.hostname) {\n // <link rel='stylesheet' href='/css/dashboard.css'>\n const stylesheet = document.createElement('link')\n stylesheet.rel = 'stylesheet'\n // Needed for the duplicate check done later.\n stylesheet.id = 'dcp-modal-styles'\n\n const dcpClientBundle = document.getElementById('_dcp_client_bundle')\n let src\n if (dcpClientBundle) {\n src = dcpClientBundle.src.replace('dcp-client-bundle.js', 'dcp-modal-style.css')\n } else {\n src = dcpConfig.portal.location.href + 'dcp-client/dist/dcp-modal-style.css'\n }\n\n stylesheet.href = src\n // If the style was injected before, don't inject it again.\n // Could occur when loading a file that imports Modal.js and loading\n // comput.min.js in the same HTML file.\n if (document.getElementById(stylesheet.id) === null) {\n document.getElementsByTagName('head')[0].appendChild(stylesheet)\n }\n\n if (typeof {\"version\":\"057ccd920bfd464401288eed9b98f438ace69b2e\",\"branch\":\"prod-20220815\",\"dcpClient\":{\"version\":\"4.2.11\",\"from\":\"git+ssh://git@gitlab.com/Distributed-Compute-Protocol/dcp-client.git#prod-20220815\",\"resolved\":\"git+ssh://git@gitlab.com/Distributed-Compute-Protocol/dcp-client.git#abc5acb71dc4f13fc06a196f98e3c58f01bed960\"},\"built\":\"Thu Aug 18 2022 09:25:47 GMT-0400 (Eastern Daylight Saving Time)\",\"config\":{\"generated\":\"Thu 18 Aug 2022 09:25:45 AM EDT by erose on lorge\",\"build\":\"debug\"},\"webpack\":\"5.70.0\",\"node\":\"v14.20.0\"} !== 'undefined' && typeof window.Modal === 'undefined') {\n window.Modal = Modal\n }\n}\n\n\n//# sourceURL=webpack://dcp/./portal/www/js/modal.js?");
3848
+ =======
3849
+ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": () => (/* binding */ Modal)\n/* harmony export */ });\n/**\n * A Small Modal Class\n * @module Modal\n */\n/* globals Event dcpConfig */\nclass Modal {\n constructor (title, message, callback = false, exitHandler = false, {\n continueLabel = 'Continue',\n cancelLabel = 'Cancel',\n cancelVisible = true\n } = {}) {\n const modal = document.createElement('div')\n modal.className = 'dcp-modal-container-old day'\n modal.innerHTML = `\n <dialog class=\"dcp-modal-content\">\n <div class=\"dcp-modal-header\">\n <h2>${title}<button type=\"button\" class=\"close\">&times;</button></h2>\n ${message ? '<p>' + message + '</p>' : ''}\n </div>\n <div class=\"dcp-modal-loading hidden\">\n <div class='loading'></div>\n </div>\n <form onsubmit='return false' method=\"dialog\">\n <div class=\"dcp-modal-body\"></div>\n <div class=\"dcp-modal-footer ${cancelVisible ? '' : 'centered'}\">\n <button type=\"submit\" class=\"continue green-modal-button\">${continueLabel}</button>\n <button type=\"button\" class=\"cancel green-modal-button\">${cancelLabel}</button>\n </div>\n </form>\n </dialog>`\n\n // To give a reference to do developer who wants to override the form submit.\n // May occur if they want to validate the information in the backend\n // without closing the modal prematurely.\n this.form = modal.querySelector('.dcp-modal-content form')\n this.continueButton = modal.querySelector('.dcp-modal-footer button.continue')\n this.cancelButton = modal.querySelector('.dcp-modal-footer button.cancel')\n this.closeButton = modal.querySelector('.dcp-modal-header .close')\n if (!cancelVisible) {\n this.cancelButton.style.display = 'none'\n }\n\n // To remove the event listener, the reference to the original function\n // added is required.\n this.formSubmitHandler = this.continue.bind(this)\n\n modal.addEventListener('keydown', function (event) {\n event.stopPropagation()\n // 27 is the keycode for the escape key.\n if (event.keyCode === 27) this.close()\n }.bind(this))\n\n this.container = modal\n this.callback = callback\n this.exitHandler = exitHandler\n document.body.appendChild(modal)\n }\n\n changeFormSubmitHandler (newFormSubmitHandler) {\n this.formSubmitHandler = newFormSubmitHandler\n }\n\n /**\n * Validates the form values in the modal and calls the modal's callback\n */\n async continue (event) {\n // To further prevent form submission from trying to redirect from the\n // current page.\n if (event instanceof Event) {\n event.preventDefault()\n }\n let fieldsAreValid = true\n let formElements = this.container.querySelectorAll('.dcp-modal-body select, .dcp-modal-body input, .dcp-modal-body textarea')\n\n const formValues = []\n if (typeof formElements.length === 'undefined') formElements = [formElements]\n // Separate into two loops to enable input validation requiring formValues\n // that come after it. e.g. Two password fields matching.\n for (let i = 0; i < formElements.length; i++) {\n switch (formElements[i].type) {\n case 'file':\n formValues.push(formElements[i])\n break\n case 'checkbox':\n formValues.push(formElements[i].checked)\n break\n default:\n formValues.push(formElements[i].value)\n break\n }\n }\n for (let i = 0; i < formElements.length; i++) {\n if (formElements[i].validation) {\n // Optional fields are allowed to be empty but still can't be wrong if not empty.\n if (!(formElements[i].value === '' && !formElements[i].required)) {\n if (typeof formElements[i].validation === 'function') {\n if (!formElements[i].validation(formValues)) {\n fieldsAreValid = false\n formElements[i].classList.add('is-invalid')\n }\n } else if (!formElements[i].validation.test(formElements[i].value)) {\n fieldsAreValid = false\n formElements[i].classList.add('is-invalid')\n }\n }\n }\n }\n\n if (!fieldsAreValid) return\n\n this.loading()\n if (typeof this.callback === 'function') {\n try {\n return this.callback(formValues)\n } catch (error) {\n console.error('Unexpected error in modal.continue:', error);\n return this.close(false)\n }\n }\n this.close(true)\n }\n\n loading () {\n this.container.querySelector('.dcp-modal-loading').classList.remove('hidden')\n this.container.querySelector('.dcp-modal-body').classList.add('hidden')\n this.container.querySelector('.dcp-modal-footer').classList.add('hidden')\n }\n\n open () {\n this.form.addEventListener('submit', async (event) => {\n const success = await this.formSubmitHandler(event)\n if (success === false) {\n return\n }\n this.close(true)\n })\n // When the user clicks on <span> (x), close the modal\n this.closeButton.addEventListener('click', this.close.bind(this))\n this.cancelButton.addEventListener('click', this.close.bind(this))\n\n // Prevent lingering outlines after clicking some form elements.\n this.container.querySelectorAll('.dcp-modal-body button, .dcp-modal-body input[type=\"checkbox\"]').forEach(element => {\n element.addEventListener('click', () => {\n element.blur()\n })\n })\n\n // Show the modal.\n this.container.style.display = 'block'\n\n const formElements = this.container.querySelectorAll('.dcp-modal-body select, .dcp-modal-body input')\n if (formElements.length) {\n formElements[0].focus()\n if (formElements[0].type === 'text') {\n formElements[0].select()\n }\n for (const el of formElements) {\n if (el.realType) {\n el.type = el.realType\n }\n }\n } else {\n // With no form elements to allow for form submission on enter, focus the\n // continue button.\n this.container.querySelector('.dcp-modal-footer button.continue').focus()\n }\n } // TODO: This should return a promise with the action resolving it\n\n /**\n * Shows the modal and returns a promise of the result of the modal (e.g. was\n * it closed, did its action succeed?)\n */\n showModal () {\n return new Promise((resolve, reject) => {\n this.form.addEventListener('submit', handleContinue.bind(this))\n this.cancelButton.addEventListener('click', handleCancel.bind(this))\n this.closeButton.addEventListener('click', handleCancel.bind(this))\n\n // Prevent lingering outlines after clicking some form elements.\n this.container.querySelectorAll('.dcp-modal-body button, .dcp-modal-body input[type=\"checkbox\"]').forEach(element => {\n element.addEventListener('click', () => {\n element.blur()\n })\n })\n\n // Show the modal.\n this.container.style.display = 'block'\n\n const formElements = this.container.querySelectorAll('.dcp-modal-body select, .dcp-modal-body input')\n if (formElements.length) {\n formElements[0].focus()\n if (formElements[0].type === 'text') {\n formElements[0].select()\n }\n for (const el of formElements) {\n if (el.realType) {\n el.type = el.realType\n }\n }\n } else {\n // With no form elements to allow for form submission on enter, focus the\n // continue button.\n this.continueButton.focus()\n }\n\n async function handleContinue (event) {\n let result\n try {\n result = await this.formSubmitHandler(event)\n } catch (error) {\n reject(error)\n }\n this.close(true)\n resolve(result)\n }\n\n async function handleCancel () {\n let result\n try {\n result = await this.close()\n } catch (error) {\n reject(error)\n }\n resolve(result)\n }\n })\n }\n\n close (success = false) {\n this.container.style.display = 'none'\n if (this.container.parentNode) {\n this.container.parentNode.removeChild(this.container)\n }\n\n // @todo this needs to remove eventlisteners to prevent memory leaks\n\n if ((success !== true) && typeof this.exitHandler === 'function') {\n return this.exitHandler(this)\n }\n }\n\n /**\n * Adds different form elements to the modal depending on the case.\n *\n * @param {*} elements - The properties of the form elements to add.\n * @returns {HTMLElement} The input form elements.\n */\n addFormElement (...elements) {\n const body = this.container.querySelector('.dcp-modal-body')\n const inputElements = []\n let label\n for (let i = 0; i < elements.length; i++) {\n let row = document.createElement('div')\n row.className = 'row'\n\n let col, input\n switch (elements[i].type) {\n case 'button':\n col = document.createElement('div')\n col.className = 'col-md-12'\n\n input = document.createElement('button')\n input.innerHTML = elements[i].label\n input.type = 'button'\n input.classList.add('green-modal-button')\n if (!elements[i].onclick) {\n throw new Error('A button in the modal body should have an on click event handler.')\n }\n input.addEventListener('click', elements[i].onclick)\n\n col.appendChild(input)\n row.appendChild(col)\n break\n case 'textarea':\n col = document.createElement('div')\n col.className = 'col-md-12'\n\n input = document.createElement('textarea')\n input.className = 'text-input-field form-control'\n if (elements[i].placeholder) input.placeholder = elements[i].placeholder\n\n col.appendChild(input)\n row.appendChild(col)\n break\n case 'text':\n case 'email':\n case 'number':\n case 'password': {\n const inputCol = document.createElement('div')\n\n input = document.createElement('input')\n input.type = elements[i].type\n input.validation = elements[i].validation\n input.autocomplete = elements[i].autocomplete || (elements[i].type === 'password' ? 'off' : 'on')\n input.className = 'text-input-field form-control'\n\n // Adding bootstraps custom feedback styles.\n let invalidFeedback = null\n if (elements[i].invalidFeedback) {\n invalidFeedback = document.createElement('div')\n invalidFeedback.className = 'invalid-feedback'\n invalidFeedback.innerText = elements[i].invalidFeedback\n }\n\n if (elements[i].type === 'password') {\n elements[i].realType = 'password'\n }\n\n if (elements[i].label) {\n const labelCol = document.createElement('div')\n label = document.createElement('label')\n label.innerText = elements[i].label\n const inputId = 'dcp-modal-input-' + this.container.querySelectorAll('input[type=\"text\"], input[type=\"email\"], input[type=\"number\"], input[type=\"password\"]').length\n label.setAttribute('for', inputId)\n input.id = inputId\n labelCol.classList.add('col-md-6', 'label-column')\n labelCol.appendChild(label)\n row.appendChild(labelCol)\n inputCol.className = 'col-md-6'\n } else {\n inputCol.className = 'col-md-12'\n }\n\n inputCol.appendChild(input)\n if (invalidFeedback !== null) {\n inputCol.appendChild(invalidFeedback)\n }\n row.appendChild(inputCol)\n break\n }\n case 'select':\n col = document.createElement('div')\n col.className = 'col-md-4'\n\n label = document.createElement('span')\n label.innerText = elements[i].label\n\n col.appendChild(label)\n row.appendChild(col)\n\n col = document.createElement('div')\n col.className = 'col-md-8'\n\n input = document.createElement('select')\n\n col.appendChild(input)\n row.appendChild(col)\n break\n case 'checkbox': {\n row.classList.add('checkbox-row')\n const checkboxLabelCol = document.createElement('div')\n checkboxLabelCol.classList.add('label-column', 'checkbox-label-column')\n\n label = document.createElement('label')\n label.innerText = elements[i].label\n label.for = 'dcp-checkbox-input-' + this.container.querySelectorAll('input[type=\"checkbox\"]').length\n label.setAttribute('for', label.for)\n label.className = 'checkbox-label'\n\n checkboxLabelCol.appendChild(label)\n\n const checkboxCol = document.createElement('div')\n checkboxCol.classList.add('checkbox-column')\n\n input = document.createElement('input')\n input.type = 'checkbox'\n input.id = label.for\n if (elements[i].checked) {\n input.checked = true\n }\n\n checkboxCol.appendChild(input)\n\n if (elements[i].labelToTheRightOfCheckbox) {\n checkboxCol.classList.add('col-md-5')\n row.appendChild(checkboxCol)\n checkboxLabelCol.classList.add('col-md-7')\n row.appendChild(checkboxLabelCol)\n } else {\n checkboxLabelCol.classList.add('col-md-6')\n checkboxCol.classList.add('col-md-6')\n row.appendChild(checkboxLabelCol)\n row.appendChild(checkboxCol)\n }\n break\n }\n case 'file':\n [input, row] = this.addFileInput(elements[i], input, row)\n break\n case 'label':\n row.classList.add('label-row')\n label = document.createElement('label')\n label.innerText = elements[i].label\n row.appendChild(label)\n break\n }\n\n // Copy other possibly specified element properties:\n const inputPropertyNames = ['title', 'inputmode', 'value', 'minLength', 'maxLength', 'size', 'required', 'pattern', 'min', 'max', 'step', 'placeholder', 'accept', 'multiple', 'id', 'onkeypress', 'oninput', 'for', 'readonly', 'autocomplete']\n for (const propertyName of inputPropertyNames) {\n if (Object.prototype.hasOwnProperty.call(elements[i], propertyName)) {\n if (propertyName === 'for' && !label.hasAttribute(propertyName)) {\n label.setAttribute(propertyName, elements[i][propertyName])\n }\n if (propertyName.startsWith('on')) {\n input.addEventListener(propertyName.slice(2), elements[i][propertyName])\n } else {\n input.setAttribute(propertyName, elements[i][propertyName])\n }\n }\n }\n\n inputElements.push(input)\n body.appendChild(row)\n }\n\n if (inputElements.length === 1) return inputElements[0]\n else return inputElements\n }\n\n /**\n * Adds a drag and drop file form element to the modal.\n *\n * @param {*} fileInputProperties - An object specifying some of the\n * properties of the file input element.\n * @param {*} fileInput - Placeholders to help create the file\n * input.\n * @param {HTMLDivElement} row - Placeholders to help create the file\n * input.\n */\n addFileInput (fileInputProperties, fileInput, row) {\n // Adding the upload label.\n const uploadLabel = document.createElement('label')\n uploadLabel.innerText = fileInputProperties.label\n row.appendChild(uploadLabel)\n const body = this.container.querySelector('.dcp-modal-body')\n body.appendChild(row)\n const fileSelectionRow = document.createElement('div')\n fileSelectionRow.id = 'file-selection-row'\n\n // Adding the drag and drop file upload input.\n const dropContainer = document.createElement('div')\n dropContainer.id = 'drop-container'\n\n // Adding an image of a wallet\n const imageContainer = document.createElement('div')\n imageContainer.id = 'image-container'\n const walletImage = document.createElement('span')\n walletImage.classList.add('fas', 'fa-wallet')\n imageContainer.appendChild(walletImage)\n\n // Adding some text prompts\n const dropMessage = document.createElement('span')\n dropMessage.innerText = 'Drop a keystore file here'\n const orMessage = document.createElement('span')\n orMessage.innerText = 'or'\n\n // Adding the manual file input element (hiding the default one)\n const fileInputContainer = document.createElement('div')\n const fileInputLabel = document.createElement('label')\n // Linking the label to the file input so that clicking on the label\n // activates the file input.\n fileInputLabel.setAttribute('for', 'file-input')\n fileInputLabel.innerText = 'Browse'\n fileInput = document.createElement('input')\n fileInput.type = fileInputProperties.type\n fileInput.id = 'file-input'\n // To remove the lingering outline after selecting the file.\n fileInput.addEventListener('click', () => {\n fileInput.blur()\n })\n fileInputContainer.append(fileInput, fileInputLabel)\n\n // Creating the final row element to append to the modal body.\n dropContainer.append(imageContainer, dropMessage, orMessage, fileInputContainer)\n fileSelectionRow.appendChild(dropContainer)\n\n // Adding functionality to the drag and drop file input.\n dropContainer.addEventListener('drop', selectDroppedFile.bind(this))\n dropContainer.addEventListener('drop', unhighlightDropArea)\n // Prevent file from being opened by the browser.\n dropContainer.ondragover = highlightDropArea\n dropContainer.ondragenter = highlightDropArea\n dropContainer.ondragleave = unhighlightDropArea\n\n fileInput.addEventListener('change', handleFileChange)\n\n const fileNamePlaceholder = document.createElement('center')\n fileNamePlaceholder.id = 'file-name-placeholder'\n fileNamePlaceholder.className = 'row'\n fileNamePlaceholder.innerText = ''\n fileSelectionRow.appendChild(fileNamePlaceholder)\n fileNamePlaceholder.classList.add('hidden')\n\n // Check if the continue button is invalid on the keystore upload modal and\n // click it if it should no longer be invalid.\n this.continueButton.addEventListener('invalid', () => {\n const fileFormElements = this.container.querySelectorAll('.dcp-modal-body input[type=\"file\"], .dcp-modal-body input[type=\"text\"]')\n const filledInFileFormElements = Array.from(fileFormElements).filter(fileFormElement => fileFormElement.value !== '')\n if (fileFormElements.length !== 0 && filledInFileFormElements.length !== 0) {\n this.continueButton.setCustomValidity('')\n // Clicking instead of dispatching a submit event to ensure other form validation is used before submitting the form.\n this.continueButton.click()\n }\n })\n\n return [fileInput, fileSelectionRow]\n\n /**\n * Checks that the dropped items contain only a single keystore file.\n * If valid, sets the file input's value to the dropped file.\n * @param {DragEvent} event - Contains the files dropped.\n */\n function selectDroppedFile (event) {\n // Prevent file from being opened.\n event.preventDefault()\n\n // Check if only one file was dropped.\n const wasOneFileDropped = event.dataTransfer.items.length === 1 ||\n event.dataTransfer.files.length === 1\n updateFileSelectionStatus(wasOneFileDropped)\n if (!wasOneFileDropped) {\n fileInput.setCustomValidity('Only one file can be uploaded.')\n fileInput.reportValidity()\n return\n } else {\n fileInput.setCustomValidity('')\n }\n\n // Now to use the DataTransfer interface to access the file(s), setting\n // the value of the file input.\n const file = event.dataTransfer.files[0]\n\n if (checkFileExtension(file)) {\n fileInput.files = event.dataTransfer.files\n fileInput.dispatchEvent(new Event('change'))\n }\n }\n\n function handleFileChange () {\n if (checkFileExtension(this.files[0]) && this.files.length === 1) {\n fileNamePlaceholder.innerText = `Selected File: ${this.files[0].name}`\n updateFileSelectionStatus(true)\n // Invoke a callback if additional functionality is required.\n if (typeof fileInputProperties.callback === 'function') {\n fileInputProperties.callback(this.files[0])\n }\n }\n }\n\n /**\n * Checks if the file extension on the inputted file is correct.\n * @param {File} file - The file to check\n * @returns {boolean} True if the file extension is valid, false otherwise.\n */\n function checkFileExtension (file) {\n // If there's no restriction, return true.\n if (!fileInputProperties.extension) {\n return true\n }\n const fileExtension = file.name.split('.').pop()\n const isValidExtension = fileExtension === fileInputProperties.extension\n updateFileSelectionStatus(isValidExtension)\n if (!isValidExtension) {\n fileInput.setCustomValidity(`Only a .${fileInputProperties.extension} file can be uploaded.`)\n fileInput.reportValidity()\n fileNamePlaceholder.classList.add('hidden')\n } else {\n fileInput.setCustomValidity('')\n }\n return isValidExtension\n }\n\n /**\n * Updates the file input to reflect the validity of the current file\n * selection.\n * @param {boolean} isValidFileSelection - True if a single .keystore file\n * was selected. False otherwise.\n */\n function updateFileSelectionStatus (isValidFileSelection) {\n imageContainer.innerHTML = ''\n const statusImage = document.createElement('span')\n statusImage.classList.add('fas', isValidFileSelection ? 'fa-check' : 'fa-times')\n statusImage.style.color = isValidFileSelection ? 'green' : 'red'\n imageContainer.appendChild(statusImage)\n\n if (!isValidFileSelection) {\n fileInput.value = null\n fileNamePlaceholder.classList.add('hidden')\n } else {\n fileNamePlaceholder.classList.remove('hidden')\n }\n\n // If the modal contains a password field for a keystore file, change its\n // visibility.\n const walletPasswordInputContainer = document.querySelector('.dcp-modal-body input[type=\"password\"]').parentElement.parentElement\n if (walletPasswordInputContainer) {\n if (isValidFileSelection) {\n walletPasswordInputContainer.classList.remove('hidden')\n const walletPasswordInput = document.querySelector('.dcp-modal-body input[type=\"password\"]')\n walletPasswordInput.focus()\n } else {\n walletPasswordInputContainer.classList.add('hidden')\n }\n }\n }\n\n function highlightDropArea (event) {\n event.preventDefault()\n this.classList.add('highlight')\n }\n\n function unhighlightDropArea (event) {\n event.preventDefault()\n this.classList.remove('highlight')\n }\n }\n\n /**\n * Sets up a custom tooltip to pop up when the passwords do not match, but are\n * valid otherwise.\n */\n addFormValidationForPasswordConfirmation () {\n const [newPassword, confirmPassword] = document.querySelectorAll('.dcp-modal-body input[type=\"password\"]')\n if (!newPassword || !confirmPassword) {\n throw Error('New Password field and Confirm Password fields not present.')\n }\n\n newPassword.addEventListener('input', checkMatchingPasswords)\n confirmPassword.addEventListener('input', checkMatchingPasswords)\n\n function checkMatchingPasswords () {\n if (newPassword.value !== confirmPassword.value &&\n newPassword.validity.valid &&\n confirmPassword.validity.valid) {\n newPassword.setCustomValidity('Both passwords must match.')\n } else if (newPassword.value === confirmPassword.value ||\n newPassword.validity.tooShort ||\n newPassword.validity.patternMismatch ||\n newPassword.validity.valueMissing ||\n confirmPassword.validity.tooShort ||\n confirmPassword.validity.patternMismatch ||\n confirmPassword.validity.valueMissing) {\n // If the passwords fields match or have become invalidated some other\n // way again, reset the custom message.\n newPassword.setCustomValidity('')\n }\n }\n }\n\n updateInvalidEmailMessage() {\n const email = document.querySelector('.dcp-modal-body input[id=\"email\"')\n if (!email){\n throw Error(\"Email field not present\")\n }\n email.addEventListener('input', checkValidEmail);\n function checkValidEmail() {\n if (!email.validity.patternMismatch &&\n !email.validity.valueMissing) {\n email.setCustomValidity('')\n } else {\n email.setCustomValidity(\"Enter a valid email address.\")\n }\n\n }\n }\n\n /**\n * Adds message(s) to the modal's body.\n * @param {string} messages - The message(s) to add to the modal's body.\n * @returns Paragraph element(s) containing the message(s) added to the\n * modal's body.\n */\n addMessage (...messages) {\n const elements = []\n const body = this.container.querySelector('.dcp-modal-body')\n for (let i = 0; i < messages.length; i++) {\n const row = document.createElement('div')\n row.className = 'row'\n\n const paragraph = document.createElement('p')\n paragraph.innerHTML = messages[i]\n paragraph.classList.add('message')\n row.appendChild(paragraph)\n body.appendChild(row)\n\n elements.push(paragraph)\n }\n\n if (elements.length === 1) return elements[0]\n else return elements\n }\n\n addHorizontalRule () {\n const body = this.container.querySelector('.dcp-modal-body')\n body.appendChild(document.createElement('hr'))\n }\n\n // Does what it says. Still ill advised to use unless you have to.\n addCustomHTML (htmlStr, browseCallback) {\n const elements = []\n const body = this.container.querySelector('.dcp-modal-body')\n body.innerHTML += htmlStr\n body.querySelector('#browse-button').addEventListener('click', browseCallback.bind(this, this))\n\n if (elements.length === 1) return elements[0]\n else return elements\n }\n\n addButton (...buttons) {\n const elements = []\n const body = this.container.querySelector('.dcp-modal-body')\n for (let i = 0; i < buttons.length; i++) {\n const row = document.createElement('div')\n row.className = 'row'\n\n let col = document.createElement('div')\n col.className = 'col-md-4'\n\n const description = document.createElement('span')\n description.innerText = buttons[i].description\n\n col.appendChild(description)\n row.appendChild(col)\n\n col = document.createElement('div')\n col.className = 'col-md-8'\n\n const button = document.createElement('button')\n button.innerText = buttons[i].label\n button.addEventListener('click', buttons[i].callback.bind(this, this))\n\n elements.push(button)\n\n col.appendChild(button)\n row.appendChild(col)\n\n body.appendChild(row)\n }\n\n if (elements.length === 1) return elements[0]\n else return elements\n }\n}\n\n\n// Inject our special stylesheet from dcp-client only if we're on the portal webpage.\nif (typeof window !== 'undefined' && typeof document !== 'undefined' && dcpConfig.portal.location.hostname === window.location.hostname) {\n // <link rel='stylesheet' href='/css/dashboard.css'>\n const stylesheet = document.createElement('link')\n stylesheet.rel = 'stylesheet'\n // Needed for the duplicate check done later.\n stylesheet.id = 'dcp-modal-styles'\n\n const dcpClientBundle = document.getElementById('_dcp_client_bundle')\n let src\n if (dcpClientBundle) {\n src = dcpClientBundle.src.replace('dcp-client-bundle.js', 'dcp-modal-style.css')\n } else {\n src = dcpConfig.portal.location.href + 'dcp-client/dist/dcp-modal-style.css'\n }\n\n stylesheet.href = src\n // If the style was injected before, don't inject it again.\n // Could occur when loading a file that imports Modal.js and loading\n // comput.min.js in the same HTML file.\n if (document.getElementById(stylesheet.id) === null) {\n document.getElementsByTagName('head')[0].appendChild(stylesheet)\n }\n\n if (typeof {\"version\":\"d4f755b478f851a75b532e258d99ea2c45d288fb\",\"branch\":\"prod-20220907\",\"dcpClient\":{\"version\":\"git+ssh://git@gitlab.com/Distributed-Compute-Protocol/dcp-client.git#ba271b8faf17b3120d92311587ea382e5e716fa0\",\"from\":\"git+ssh://git@gitlab.com/Distributed-Compute-Protocol/dcp-client.git#prod-20220907\",\"resolved\":\"git+ssh://git@gitlab.com/Distributed-Compute-Protocol/dcp-client.git#ba271b8faf17b3120d92311587ea382e5e716fa0\"},\"built\":\"Thu Sep 08 2022 16:23:20 GMT-0400 (Eastern Daylight Saving Time)\",\"config\":{\"generated\":\"Thu 08 Sep 2022 04:23:18 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?");
3850
+ >>>>>>> origin/prod-20220907
3835
3851
 
3836
3852
  /***/ }),
3837
3853
 
@@ -3873,7 +3889,7 @@ eval("/** \n * Factory function which creates instances of the future function t
3873
3889
  /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
3874
3890
 
3875
3891
  "use strict";
3876
- eval("/**\n * @file dcp-assert.js\n * Simple assertion module for DCP. Assertions are only\n * evaluated for debug builds, except for the security-named\n * assertions.\n *\n * @author Wes Garland, wes@kingsds.network\n * @date Dec 2019\n */\n\n\nvar isDebugBuild = undefined;\nvar patchup = undefined;\n\nexports.assertTriggersDebugger = !!(__webpack_require__(/*! process */ \"./node_modules/process/browser.js\").env.DCP_ASSERT_TRIGGERS_DEBUGGER);\nexports.assertTestTriggersDebugger = !!(__webpack_require__(/*! process */ \"./node_modules/process/browser.js\").env.DCP_ASSERT_TEST_TRIGGERS_DEBUGGER);\n\n/** Sets whether this assertion module is running in a debug build or not. It not called\n * before the first assertion, then we figure this out by checking the module that DCP\n * was configured in with ./configure.sh. The difference is that non-security assertions\n * are ignored during production builds.\n *\n * This function must only be invoked once and only before the first assertion; in all \n * other cases, it will throw an exception.\n *\n * @param idb {boolean} false if this is release build; all other\n * values imply a debug build.\n */\nexports.setDebugBuild = function dcpAssert$$setDebugBuild(idb) {\n if (typeof isDebugBuild !== 'undefined')\n throw new Error('dcp-assert module was already initialized');\n isDebugBuild = (idb !== false);\n\n if (!isDebugBuild) {\n let dummy = function dcpAssert$dummy(){return};\n for (let assertion of assertionList) {\n if (typeof exports[assertion] === 'function')\n exports[assertion] = dummy;\n }\n } else {\n for (let assertion of assertionList)\n exports[assertion] = exports.always[assertion];\n }\n}\n \n/** Generic assertion mechanism. Throws if any argument is not true. */\nlet assert = exports.assert = function dcpAssert$$assert() {\n let e;\n\n if (exports.assertTestTriggersDebugger)\n debugger; // allow-debugger\n \n if (patchup) {\n if (!patchup())\n return;\n }\n \n for (let value of arguments) {\n if (!value) {\n if (exports.assertTriggersDebugger)\n debugger; // allow-debugger\n\n try { /* this throws in ES5 strict mode and maybe future-ES */\n let loc = 2;\n if (Object.keys(exports).map((key) => exports[key]).includes(arguments.callee.caller))\n loc++;\n e = new Error('Assertion failure ' + new Error().stack.toString().split('\\n')[loc].trim());\n } catch(error) {\n e = new Error('Assertion failure');\n }\n e.code = 'EASSERT';\n throw e;\n }\n }\n}\n\n/** Evaluate an expression; assert if the result is not true */\nexports.assertEval = function dcpAssert$$assertEval(expr) {\n assert(eval(expr));\n}\n\n/** Assert to that two values are == equivalent */\nexports.assertEq2 = function dcpAssert$$assertEq2(lValue, rValue) {\n assert(lValue == rValue)\n}\n\n/**\n * Asserts that two values are the same in terms of strict equality (===).\n * Can pass an optional message describing the assertion being made.\n *\n * @param {any} expected The expected value to test for equality\n * @param {any} actual The actual value to compare teh expected value against\n * @param {string} [message=''] An message appended to the assertion error\n */\nexports.assertEq3 = function dcpAssert$$assertEq3(\n expected,\n actual,\n message = '',\n) {\n try {\n assert(expected === actual);\n } catch (e) {\n if (message) {\n e.message += `: ${message}`;\n }\n\n e.message += ` (${expected} !== ${actual})`;\n throw e;\n }\n};\n\n/** Assert to that two values are not == equivalent */\nexports.assertNeq2 = function dcpAssert$$assertNeq2(lValue, rValue) {\n assert(lValue != rValue);\n}\n\n/** Assert to that two values are not the same */\nexports.assertNeq3 = function dcpAssert$$assertNeq3(lValue, rValue) {\n assert(lValue !== rValue);\n}\n\n/**\n * Assertion that ensures a given statement will throw a given exception.\n * @param statement {function} function to invoke which is expected to throw\n * @param statement {string} source code of statement which is evaluated with direct-eval\n * and expected to throw\n * @param code [optional] {string} expected value of the exception's code property\n * @param ctor [optional] {function} function which is expected on the exceptions prototype chain\n * @returns true if expectations were met\n */\nexports.assertThrows = function dcpAssert$$assertThrows(statement, code, ctor) {\n var threw;\n \n if (typeof statement === 'string') {\n statement = function shouldThrow_statement() { eval(arguments[0]) };\n }\n if (arguments.length === 2 && typeof code === 'function') {\n ctor = code;\n code = undefined;\n }\n\n try {\n let result = statement();\n threw = false;\n } catch(e) {\n threw = true;\n if (code)\n assert(e.code === code);\n if (ctor)\n assert(e instanceof ctor);\n }\n\n assert(threw === true);\n}\n\n/**\n * Assertion that ensures a given collection contains a given element.\n *\n * @param {any} haystack The collection to search; must be a Set, Map, Array or Array-like object.\n * @param {any} needle The element to search for\n */\nexports.assertHas = function dcpAssert$$assertHas(haystack, needle) {\n if (Array.isArray(haystack))\n assert(haystack.indexOf(needle) !== -1);\n else if (needle instanceof Set || needle instanceof Map)\n assert(haystack.has(needle));\n else\n assert(Array.from(haystack).indexOf(needle) !== -1);\n}\n\n/**\n * Assertion that ensures a given value is of a given type.\n */\nexports.assertIsA = function dcpAssert$$assertIsA(value, type) {\n assert(typeof value === type);\n}\n\n/* *** All assertions must be defined above here *** */\nconst assertionList = Object.keys(exports);\n\n/** Add the security assertions (not disabled by debug build) */\nfor (let assertion of assertionList) {\n let securityAssertion = 'security' + assertion[0].toUpperCase() + assertion.slice(1);\n exports[securityAssertion] = exports[assertion];\n}\n\n/** Add the 'always' assertions (also not disabled by debug build) */\nexports.always = {};\nfor (let assertion of assertionList) {\n exports.always[assertion] = exports[assertion];\n}\n\n/** This function runs one event loop tick after module initialization, to give us the chance to set the debug mode properly. */\npatchup = function dcpAssert$$patchup()\n{\n const build = (__webpack_require__(/*! ./dcp-build */ \"./src/common/dcp-build.js\").build);\n\n if (typeof build !== 'string')\n throw new Error('build should be string but is', typeof build);\n \n try {\n isDebugBuild = isDebugBuild || (build === 'debug');\n patchup = false;\n if (typeof isDebugBuild === 'undefined')\n exports.setDebugBuild(isDebugBuild);\n } catch(e) {\n console.log(e);\n };\n return isDebugBuild;\n}\n\n/* This wart is the result of our bad habit of mutating things other than module memos during module initialization */\n__webpack_require__(/*! ./dcp-timers */ \"./src/common/dcp-timers.js\").setImmediate(patchup);\n\n\n//# sourceURL=webpack://dcp/./src/common/dcp-assert.js?");
3892
+ eval("/**\n * @file dcp-assert.js\n * Simple assertion module for DCP. Assertions are only\n * evaluated for debug builds, except for the security-named\n * assertions.\n *\n * @author Wes Garland, wes@kingsds.network\n * @date Dec 2019\n */\n\n\nvar isDebugBuild = undefined;\n\nexports.assertTriggersDebugger = !!(__webpack_require__(/*! process */ \"./node_modules/process/browser.js\").env.DCP_ASSERT_TRIGGERS_DEBUGGER);\nexports.assertTestTriggersDebugger = !!(__webpack_require__(/*! process */ \"./node_modules/process/browser.js\").env.DCP_ASSERT_TEST_TRIGGERS_DEBUGGER);\n\n/** \n * Sets whether this assertion module is running in a debug mode or not. If not called\n * before the first assertion, then we figure this out by checking the module that DCP\n * was configured in with ./configure.sh. The difference is that non-security assertions\n * are ignored during production builds.\n *\n * @note this code test to run very early in the process due to eager module initialization,\n * especially while loading the webpack bundle, which might not have actually set\n * the correct dcpConfig.build yet, so it's stuck at \"bootstrap\", which we treat mostly\n * like a debug build.\n * \n * @param idb {boolean|undefined} falsey if this is release build; truey if this is a debug build; undefined to detect\n */\nexports.setDebugBuild = function dcpAssert$$setDebugBuild(idb)\n{\n if (typeof idb === 'undefined')\n idb = dcpConfig.build !== 'release';\n \n /* In release mode, rewrite the non-security assertions as fast dummy functions */\n if (!idb)\n {\n let dummy = function dcpAssert$dummy(){return};\n for (let assertion of assertionList) {\n if (typeof exports[assertion] === 'function')\n exports[assertion] = dummy;\n }\n }\n else\n {\n for (let assertion of assertionList)\n exports[assertion] = exports.always[assertion];\n }\n\n if (dcpConfig.build !== 'bootstrap')\n isDebugBuild = Boolean(idb); \n}\n \n/** Generic assertion mechanism. Throws if any argument is not true. */\nlet assert = exports.assert = function dcpAssert$$assert() {\n let e;\n\n if (typeof isDebugBuild === 'undefined')\n exports.setDebugBuild();\n \n if (exports.assertTestTriggersDebugger)\n debugger; // allow-debugger\n \n for (let value of arguments) {\n if (!value) {\n if (exports.assertTriggersDebugger)\n debugger; // allow-debugger\n\n try { /* this throws in ES5 strict mode and maybe future-ES */\n let loc = 2;\n if (Object.keys(exports).map((key) => exports[key]).includes(arguments.callee.caller))\n loc++;\n e = new Error('Assertion failure ' + new Error().stack.toString().split('\\n')[loc].trim());\n } catch(error) {\n e = new Error('Assertion failure');\n }\n e.code = 'EASSERT';\n throw e;\n }\n }\n}\n\n/** Evaluate an expression; assert if the result is not true */\nexports.assertEval = function dcpAssert$$assertEval(expr) {\n assert(eval(expr));\n}\n\n/** Assert to that two values are == equivalent */\nexports.assertEq2 = function dcpAssert$$assertEq2(lValue, rValue) {\n assert(lValue == rValue)\n}\n\n/**\n * Asserts that two values are the same in terms of strict equality (===).\n * Can pass an optional message describing the assertion being made.\n *\n * @param {any} expected The expected value to test for equality\n * @param {any} actual The actual value to compare teh expected value against\n * @param {string} [message=''] An message appended to the assertion error\n */\nexports.assertEq3 = function dcpAssert$$assertEq3(\n expected,\n actual,\n message = '',\n) {\n try {\n assert(expected === actual);\n } catch (e) {\n if (message) {\n e.message += `: ${message}`;\n }\n\n e.message += ` (${expected} !== ${actual})`;\n throw e;\n }\n};\n\n/** Assert to that two values are not == equivalent */\nexports.assertNeq2 = function dcpAssert$$assertNeq2(lValue, rValue) {\n assert(lValue != rValue);\n}\n\n/** Assert to that two values are not the same */\nexports.assertNeq3 = function dcpAssert$$assertNeq3(lValue, rValue) {\n assert(lValue !== rValue);\n}\n\n/**\n * Assertion that ensures a given statement will throw a given exception.\n * @param statement {function} function to invoke which is expected to throw\n * @param statement {string} source code of statement which is evaluated with direct-eval\n * and expected to throw\n * @param code [optional] {string} expected value of the exception's code property\n * @param ctor [optional] {function} function which is expected on the exceptions prototype chain\n * @returns true if expectations were met\n */\nexports.assertThrows = function dcpAssert$$assertThrows(statement, code, ctor) {\n var threw;\n \n if (typeof statement === 'string') {\n statement = function shouldThrow_statement() { eval(arguments[0]) };\n }\n if (arguments.length === 2 && typeof code === 'function') {\n ctor = code;\n code = undefined;\n }\n\n try {\n let result = statement();\n threw = false;\n } catch(e) {\n threw = true;\n if (code)\n assert(e.code === code);\n if (ctor)\n assert(e instanceof ctor);\n }\n\n assert(threw === true);\n}\n\n/**\n * Assertion that ensures a given collection contains a given element.\n *\n * @param {any} haystack The collection to search; must be a Set, Map, Array or Array-like object.\n * @param {any} needle The element to search for\n */\nexports.assertHas = function dcpAssert$$assertHas(haystack, needle) {\n if (Array.isArray(haystack))\n assert(haystack.indexOf(needle) !== -1);\n else if (needle instanceof Set || needle instanceof Map)\n assert(haystack.has(needle));\n else\n assert(Array.from(haystack).indexOf(needle) !== -1);\n}\n\n/**\n * Assertion that ensures a given value is of a given type.\n */\nexports.assertIsA = function dcpAssert$$assertIsA(value, type) {\n assert(typeof value === type);\n}\n\n/* *** All assertions must be defined above here *** */\nconst assertionList = Object.keys(exports);\n\n/** Add the security assertions (not disabled by debug build) */\nfor (let assertion of assertionList) {\n let securityAssertion = 'security' + assertion[0].toUpperCase() + assertion.slice(1);\n exports[securityAssertion] = exports[assertion];\n}\n\n/** Add the 'always' assertions (also not disabled by debug build) */\nexports.always = {};\nfor (let assertion of assertionList) {\n exports.always[assertion] = exports[assertion];\n}\n\n\n//# sourceURL=webpack://dcp/./src/common/dcp-assert.js?");
3877
3893
 
3878
3894
  /***/ }),
3879
3895
 
@@ -3923,7 +3939,8 @@ eval("/* provided dependency */ var process = __webpack_require__(/*! ./node_mod
3923
3939
  \*********************************/
3924
3940
  /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
3925
3941
 
3926
- eval("/**\n * @file src/dcp-error.js\n * @author Ryan Rossiter, ryan@kingsds.network\n * @date Feb 2020\n *\n * A super-simple error class with a constructor that accepts an error 'code'\n * and 'info' in addition to the message. Also has another form which accepts an\n * error object to wrap as is second argument which is helpful for dealing with\n * Error Payloads.\n */\n\nconst { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\n\nexports.DCPError = class DCPError extends Error {\n /**\n * @param {string} [message] - The error message\n * @param {Array<string | object> | Array<object> | Array} [args] - An array consisting of\n * an error code and info object, or an Error object to wrap.\n */\n constructor(message, ...args) {\n super(message);\n this.name = DCPError.name;\n this.timestamp = new Date();\n switch (typeof args[0]) {\n // Form 1: Error code and info object\n case 'string': {\n const [code = '', info = null] = args;\n if (code !== '') {\n this.code = code;\n }\n\n if (info !== null) {\n this.info = info;\n }\n this.stack = this.stack.replace(/\\n[^\\n]*\\n/, ''); /* remove constructor from stack */\n break;\n }\n // Form 2: Error object to wrap\n case 'object': {\n const [error] = args;\n if (error.code) {\n this.code = error.code;\n }\n\n if (error.info) {\n this.info = error.info;\n }\n\n if (error.process) {\n this.process = error.process;\n }\n\n if (error.timestamp) {\n this.timestamp = error.timestamp;\n }\n\n this.stack = `${this.stack}\\nRethrow ${error.stack ? error.stack : 'stack is not defined'}`;\n break;\n }\n default:\n break;\n }\n\n if (!this.process) {\n try {\n if ((__webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\").isBrowserPlatform)) {\n this.process = `<browser ${window.location.pathname}>`;\n } else {\n this.process = requireNative('path').basename(\n requireNative('process').argv[1] || '<node repl>',\n );\n }\n } catch (e) {\n console.error(e);\n }\n }\n }\n};\n\n\n//# sourceURL=webpack://dcp/./src/common/dcp-error.js?");
3942
+ "use strict";
3943
+ eval("/**\n * @file src/dcp-error.js\n * @author Ryan Rossiter, ryan@kingsds.network\n * @date Feb 2020\n *\n * A super-simple error class with a constructor that accepts an error 'code'\n * and 'info' in addition to the message. Also has another form which accepts an\n * error object to wrap as is second argument which is helpful for dealing with\n * Error Payloads.\n */\n\n\nexports.DCPError = class DCPError extends Error {\n /**\n * @param {string} [message] - The error message\n * @param {Array<string | object> | Array<object> | Array} [args] - An array consisting of\n * an error code and info object, or an Error object to wrap.\n */\n constructor(message, ...args) {\n super(message);\n this.name = DCPError.name;\n this.timestamp = new Date();\n switch (typeof args[0]) {\n // Form 1: Error code and info object\n case 'string': {\n const [code = '', info = null] = args;\n if (code !== '') {\n this.code = code;\n }\n\n if (info !== null) {\n this.info = info;\n }\n this.stack = this.stack.replace(/\\n[^\\n]*\\n/, ''); /* remove constructor from stack */\n break;\n }\n // Form 2: Error object to wrap\n case 'object': {\n const [error] = args;\n if (error.code) {\n this.code = error.code;\n }\n\n if (error.info) {\n this.info = error.info;\n }\n\n if (error.process) {\n this.process = error.process;\n }\n\n if (error.timestamp) {\n this.timestamp = error.timestamp;\n }\n\n this.stack = `${this.stack}\\nRethrow ${error.stack ? error.stack : 'stack is not defined'}`;\n break;\n }\n default:\n break;\n }\n\n if (!this.process) {\n try {\n if ((__webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\").isBrowserPlatform)) {\n this.process = `<browser ${window.location.pathname}>`;\n } else {\n const { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\n this.process = requireNative('path').basename(\n requireNative('process').argv[1] || '<node repl>',\n );\n }\n } catch (e) {\n console.error(e);\n }\n }\n }\n};\n\n\n//# sourceURL=webpack://dcp/./src/common/dcp-error.js?");
3927
3944
 
3928
3945
  /***/ }),
3929
3946
 
@@ -3934,7 +3951,7 @@ eval("/**\n * @file src/dcp-error.js\n * @author Ryan Rossiter, ryan
3934
3951
  /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
3935
3952
 
3936
3953
  "use strict";
3937
- eval("/**\n * @author Wes Garland, wes@page.ca\n * @date Dec 2012, Jun 2018\n */\n\n\nvar debugBuild = Boolean(globalThis.hasOwnProperty('dcpConfig') && dcpConfig.build === 'debug');\n\nconst LISTENERS = '__listeners';\nconst caseSensitiveEventNames = false; /* default false - DOM-level-2 compliant */\n\nexports.breakpointEvents = {};\n\n/** An EventEmitter class which is API-compatible with Node's EventEmitter class. \n * This class essentially registers and executes callbacks by attaching them to strings.\n * There is no standard `event` object at the moment, so callbacks simply are passed whatever args the \n * registered event passes.\n * @memberof module:dcp/dcp-events\n * @access public\n */\nclass EventEmitter {\n constructor(debugLabel) {\n this.debugLabel = debugLabel || \"\";\n if (true) {\n this.debug = ((__webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope)('event-emitter'))(this.debugLabel || 'all');\n }\n }\n\n /** Add an event listener. \n * @param {string} eventName The name of the event to capture, or true to capture all events\n * @param {function} eventHandler The function to invoke when the eventHandler is invoked. The arguments\n * to the function are specified by the code emitting the event, however\n * a final argument, eventName, is appended to event handlers invoked via\n * catch-alls. \n * @access public\n */\n addListener(eventName, eventHandler) {\n\n if (!this.hasOwnProperty(LISTENERS))\n {\n this[LISTENERS] = {}; /* must not be moved to ctor! */\n if (debugBuild && !this.breakpointEvents)\n this.breakpointEvents = {};\n }\n \n eventName = sanitizeEventName(eventName);\n if (!this[LISTENERS].hasOwnProperty(eventName))\n this[LISTENERS][eventName] = [];\n\n if (typeof eventHandler !== \"function\")\n throw new Error(\"eventHandler is not a function\");\n\n this[LISTENERS][eventName].push(eventHandler);\n this.emit(\"newListener\", eventName, eventHandler);\n }\n\n /**\n * Alias of {@link module:dcp/dcp-events.EventEmitter#addListener|addListener}.\n * @access public\n */\n on(eventName, eventHandler) {\n this.addListener(eventName, eventHandler);\n }\n\n /**\n * Alias of {@link module:dcp/dcp-events.EventEmitter#addListener|addListener}.\n * @access public\n */\n addEventListener(eventName, eventHandler) {\n this.addListener(eventName, eventHandler);\n }\n\n /**\n * Attach a callback (eventHandler) to a named event. It will be called once when this event emitter\n * emits that event and then removed.\n * @param {string} eventName Name of event\n * @param {function} eventHandler Function to call\n * @access public\n */\n once(eventName, eventHandler) {\n function onceWrapper() {\n eventHandler.apply(eventHandler, arguments);\n this.removeListener(eventName, onceWrapper);\n }\n this.addListener(eventName, onceWrapper);\n return onceWrapper;\n }\n\n /**\n * Remove a listener (`eventHandler`) attached to a named event. It must\n * be the same `eventHandler` instance that was originally attached.\n * @param {string} eventName Name of event\n * @param {function} eventHandler Function to call\n * @access public\n */\n removeListener(eventName, eventHandler) {\n var i;\n\n eventName = sanitizeEventName(eventName);\n if (!this[LISTENERS] || !this[LISTENERS].hasOwnProperty(eventName))\n return;\n\n i = this[LISTENERS][eventName].indexOf(eventHandler);\n if (i != -1)\n {\n this[LISTENERS][eventName].splice(i, 1);\n if (this[LISTENERS][eventName].length === 0)\n delete this[LISTENERS][eventName];\n }\n }\n\n /**\n * Alias of {@link module:dcp/dcp-events.EventEmitter#removeListener|removeListener}.\n * @access public\n */\n off(eventName, eventHandler) {\n this.removeListener(eventName, eventHandler)\n }\n\n /**\n * Returns an array listing the events for which the emitter has registered listeners.\n * The values in the array are strings.\n */\n eventNames()\n {\n return Object.keys(this[LISTENERS]);\n }\n\n /**\n * Alias of {@link module:dcp/dcp-events.EventEmitter#removeListener|removeListener}.\n * @access public\n */\n removeEventListener(eventName, eventHandler) {\n this.removeListener(eventName, eventHandler)\n }\n\n removeAllListeners(eventName) {\n if (typeof eventName !== 'undefined') {\n eventName = sanitizeEventName(eventName);\n if (this[LISTENERS])\n delete this[LISTENERS][eventName];\n } else {\n this[LISTENERS] = {};\n }\n }\n\n listeners(eventName) {\n if (!this[LISTENERS])\n return [];\n \n eventName = sanitizeEventName(eventName);\n if (!this[LISTENERS].hasOwnProperty(eventName))\n this[LISTENERS][eventName] = [];\n\n return this[LISTENERS][eventName];\n }\n\n setMaxListeners(number) {\n throw new Error(\"not implemented\");\n }\n\n listenerCount(eventName) {\n if (!this[LISTENERS])\n return 0;\n \n eventName = sanitizeEventName(eventName);\n return this[LISTENERS][eventName]? this[LISTENERS][eventName].length : 0;\n }\n\n /** Event emitter. All interfaces which emit events eventually fall through to this function. \n * This function invokes event listeners in the correct context for the passed eventName, as\n * well as any catch-all (true) listeners.\n *\n * @param _eventName The name of the event to emit.\n * @param args An array (not array-like) object containing the arguments \n * to pass to the listener.\n */\n vemit(_eventName, args)\n {\n var i;\n const eventName = sanitizeEventName(_eventName);\n\n if (!this[LISTENERS] || !(this[LISTENERS].hasOwnProperty(eventName) || this[LISTENERS].hasOwnProperty(true)))\n {\n if (this.debug && _eventName !== 'newListener')\n console.debug(\"Not firing \" + this.debugLabel + \"::\" + _eventName + \" -- no listeners\");\n return;\n }\n\n if (this.debug)\n console.debug(\"Firing \" + this.debugLabel + \"::\" + _eventName);\n\n if (this[LISTENERS].hasOwnProperty(eventName))\n {\n for (i=0; i < this[LISTENERS][eventName].length; i++)\n {\n const realEventHandler = this[LISTENERS][eventName][i];\n let eventHandler = !debugBuild ? realEventHandler : function eventHandlerWrapper() {\n if (exports.breakpointEvents[eventName] || this.breakpointEvents[eventName])\n debugger; // allow-debugger\n return realEventHandler.apply(this, arguments);\n };\n this.runSoon(this, eventHandler, args);\n }\n }\n \n if (typeof this[LISTENERS][true] !== \"undefined\")\n {\n for (i=0; i < this[LISTENERS][true].length; i++)\n this.runSoon(this, this[LISTENERS][true][i], args.concat(eventName));\n }\n }\n\n /** Emit an event. \n *\n * @param {string} eventName The event to emit\n * @param ... Any other arguments passed will be passed exactly to the listener.\n * @access public\n */\n emit(eventName /* ... */) {\n var args;\n\n args = Array.prototype.slice.call(arguments);\n args.shift();\n\n return this.vemit(eventName, args);\n }\n\n /** Proxy all of the events from another EventEmitter or DOM EventTarget and \n * re-emit them as though they originated from this EventEmitter.\n * \n * @param element The EventEmitter / EventTarget whose events we are to re-emit\n * @param eventName ... One or more arguments which are strings that contain the names of\n * the events to re-emit.\n */\n proxy(element, eventName /*, eventName... */) {\n var i, fun;\n var _this = this;\n\n for (i=1; i < arguments.length; i++) {\n fun = (function events$$EventEmitter$proxy$fun(thisEventName) {\n sanitizeEventName(thisEventName);\n return (function events$$EventEmitter$proxy$fun$bound() {\n if (typeof arguments[0] === 'object')\n arguments[0].proxyType = thisEventName;\n this.vemit(thisEventName, arguments) \n }).bind(_this);\n })(arguments[i]);\n element.addEventListener(arguments[i], fun);\n }\n }\n\n /** Proxy an events from another EventEmitter or DOM EventTarget and \n * re-emit them as though they originated from this EventEmitter, under a new event type.\n * \n * @param element The EventEmitter / EventTarget whose events we are to re-emit\n * @param captureEventName The name of the event to re-emit, or true to re-emit all events\n * @param newEventName The event type of the new event when eventName is not true; when\n * eventName is true, it is used as a prefix prepended to the real\n * event name (ev.type)\n *\n * *note* - Catch-all proxies are only supported on instances of events$$EventEmitter. That\n * functionality is not supported by DOM events.\n */\n proxyAs(element, captureEventName, emitterEventName) {\n var fun;\n captureEventName = sanitizeEventName(captureEventName);\n emitterEventName = sanitizeEventName(emitterEventName);\n \n if (element === this && captureEventName === true)\n throw new Error(\"Infinite recursion is not supported\");\n\n fun = (function events$$EventEmitter$proxyAs$eventHandler(ev) {\n var args = Array.prototype.slice.call(arguments);\n\n if (typeof ev === 'object' && ev.hasOwnProperty('type'))\n ev.proxyType = ev.type;\n\n if (captureEventName === true)\n this.vemit(emitterEventName + args.pop(), args);\n else\n this.vemit(emitterEventName, arguments);\n }).bind(this);\n\n element.addEventListener(captureEventName, fun);\n }\n\n /** Run this supplied function ASAP in the reactor loop, with the provided 'this' and arguments.\n * @param _this [optional] An object to use apply as 'this' when invoking fn\n * @param fn The function to invoke\n * @param args Array of arguments passed to fn during invocation\n */\n runSoon(_this, fn, args) {\n var callback;\n\n if (typeof _this === \"function\" && typeof args === 'undefined') {\n args = fn;\n fn = _this;\n _this = null;\n }\n\n if (typeof fn !== \"function\")\n throw new Error(\"fn is not a function\");\n \n if (typeof args === 'object')\n callback = function runSoon_inner() { return fn.apply(_this, args) };\n else\n callback = function runSoon_inner() { return fn.apply(_this, []) };\n \n return this.doCallback(callback);\n }\n\n get doCallback() { \n return typeof setImmediate === 'function' ? setImmediate : (cb) => { setTimeout(cb, 0) };\n }\n\n /** Debug interface - not stable */\n dumpKnownEvents() {\n var eventName, i;\n var list = [];\n\n if (!this[LISTENERS] || !this[LISTENERS].length)\n return list;\n\n for (eventName in this[LISTENERS])\n list.push(eventName);\n \n list.sort();\n for (i=0; i < list.length; i++) {\n if (list[i] == list[i+1])\n list.splice(i+1,1);\n }\n\n return list;\n }\n\n breakOn(eventName)\n {\n if (!debugBuild)\n throw new Error('breakOn feature not available in release builds');\n \n eventName = sanitizeEventName(eventName);\n if (!this.breakpointEvents)\n this.breakpointEvents = {};\n this.breakpointEvents[eventName] = true;\n }\n}\n\nfunction sanitizeEventName(eventName)\n{\n if (!caseSensitiveEventNames)\n eventName = String(eventName).toLowerCase();\n return eventName;\n}\n\nexports.EventEmitter = EventEmitter;\nexports.caseSensitiveEventNames = caseSensitiveEventNames;\nexports.sanitizeEventName = sanitizeEventName;\n\n\n//# sourceURL=webpack://dcp/./src/common/dcp-events/event-emitter.js?");
3954
+ eval("/**\n * @author Wes Garland, wes@page.ca\n * @date Dec 2012, Jun 2018\n */\n\n\nvar debugBuild = Boolean(globalThis.hasOwnProperty('dcpConfig') && dcpConfig.build === 'debug');\n\nconst LISTENERS = '__listeners';\nconst caseSensitiveEventNames = false; /* default false - DOM-level-2 compliant */\n\nexports.breakpointEvents = {};\n\n/** An EventEmitter class which is API-compatible with Node's EventEmitter class. \n * This class essentially registers and executes callbacks by attaching them to strings.\n * There is no standard `event` object at the moment, so callbacks simply are passed whatever args the \n * registered event passes.\n * @memberof module:dcp/dcp-events\n * @access public\n */\nclass EventEmitter {\n constructor(debugLabel) {\n this.debugLabel = debugLabel || \"\";\n if (true) {\n this.debug = ((__webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope)('event-emitter'))(this.debugLabel || 'all');\n }\n }\n\n /** Add an event listener. \n * @param {string} eventName The name of the event to capture, or true to capture all events\n * @param {function} eventHandler The function to invoke when the eventHandler is invoked. The arguments\n * to the function are specified by the code emitting the event, however\n * a final argument, eventName, is appended to event handlers invoked via\n * catch-alls. \n * @access public\n */\n addListener(eventName, eventHandler) {\n\n if (!this.hasOwnProperty(LISTENERS))\n {\n this[LISTENERS] = {}; /* must not be moved to ctor! */\n if (debugBuild && !this.breakpointEvents)\n this.breakpointEvents = {};\n }\n \n eventName = sanitizeEventName(eventName);\n if (!this[LISTENERS].hasOwnProperty(eventName))\n this[LISTENERS][eventName] = [];\n\n if (typeof eventHandler !== \"function\")\n throw new Error(\"eventHandler is not a function\");\n\n this[LISTENERS][eventName].push(eventHandler);\n this.emit(\"newListener\", eventName, eventHandler);\n }\n\n /**\n * Alias of {@link module:dcp/dcp-events.EventEmitter#addListener|addListener}.\n * @access public\n */\n on(eventName, eventHandler) {\n this.addListener(eventName, eventHandler);\n }\n\n /**\n * Alias of {@link module:dcp/dcp-events.EventEmitter#addListener|addListener}.\n * @access public\n */\n addEventListener(eventName, eventHandler) {\n this.addListener(eventName, eventHandler);\n }\n\n /**\n * Attach a callback (eventHandler) to a named event. It will be called once when this event emitter\n * emits that event and then removed.\n * @param {string} eventName Name of event\n * @param {function} eventHandler Function to call\n * @access public\n */\n once(eventName, eventHandler) {\n const that = this;\n function onceWrapper() {\n eventHandler.apply(eventHandler, arguments);\n that.removeListener(eventName, onceWrapper);\n }\n this.addListener(eventName, onceWrapper);\n return onceWrapper;\n }\n\n /**\n * Remove a listener (`eventHandler`) attached to a named event. It must\n * be the same `eventHandler` instance that was originally attached.\n * @param {string} eventName Name of event\n * @param {function} eventHandler Function to call\n * @access public\n */\n removeListener(eventName, eventHandler) {\n var i;\n\n eventName = sanitizeEventName(eventName);\n if (!this[LISTENERS] || !this[LISTENERS].hasOwnProperty(eventName))\n return;\n\n i = this[LISTENERS][eventName].indexOf(eventHandler);\n if (i != -1)\n {\n this[LISTENERS][eventName].splice(i, 1);\n if (this[LISTENERS][eventName].length === 0)\n delete this[LISTENERS][eventName];\n }\n }\n\n /**\n * Alias of {@link module:dcp/dcp-events.EventEmitter#removeListener|removeListener}.\n * @access public\n */\n off(eventName, eventHandler) {\n this.removeListener(eventName, eventHandler)\n }\n\n /**\n * Returns an array listing the events for which the emitter has registered listeners.\n * The values in the array are strings.\n */\n eventNames()\n {\n return Object.keys(this[LISTENERS]);\n }\n\n /**\n * Alias of {@link module:dcp/dcp-events.EventEmitter#removeListener|removeListener}.\n * @access public\n */\n removeEventListener(eventName, eventHandler) {\n this.removeListener(eventName, eventHandler)\n }\n\n removeAllListeners(eventName) {\n if (typeof eventName !== 'undefined') {\n eventName = sanitizeEventName(eventName);\n if (this[LISTENERS])\n delete this[LISTENERS][eventName];\n } else {\n this[LISTENERS] = {};\n }\n }\n\n listeners(eventName) {\n if (!this[LISTENERS])\n return [];\n \n eventName = sanitizeEventName(eventName);\n if (!this[LISTENERS].hasOwnProperty(eventName))\n this[LISTENERS][eventName] = [];\n\n return this[LISTENERS][eventName];\n }\n\n setMaxListeners(number) {\n throw new Error(\"not implemented\");\n }\n\n listenerCount(eventName) {\n if (!this[LISTENERS])\n return 0;\n \n eventName = sanitizeEventName(eventName);\n return this[LISTENERS][eventName]? this[LISTENERS][eventName].length : 0;\n }\n\n /** Event emitter. All interfaces which emit events eventually fall through to this function. \n * This function invokes event listeners in the correct context for the passed eventName, as\n * well as any catch-all (true) listeners.\n *\n * @param _eventName The name of the event to emit.\n * @param args An array (not array-like) object containing the arguments \n * to pass to the listener.\n */\n vemit(_eventName, args)\n {\n var i;\n const eventName = sanitizeEventName(_eventName);\n\n if (!this[LISTENERS] || !(this[LISTENERS].hasOwnProperty(eventName) || this[LISTENERS].hasOwnProperty(true)))\n {\n if (this.debug && _eventName !== 'newListener')\n console.debug(\"Not firing \" + this.debugLabel + \"::\" + _eventName + \" -- no listeners\");\n return;\n }\n\n if (this.debug)\n console.debug(\"Firing \" + this.debugLabel + \"::\" + _eventName);\n\n if (this[LISTENERS].hasOwnProperty(eventName))\n {\n for (i=0; i < this[LISTENERS][eventName].length; i++)\n {\n const realEventHandler = this[LISTENERS][eventName][i];\n const that = this;\n let eventHandler = !debugBuild ? realEventHandler : function eventHandlerWrapper() {\n if (exports.breakpointEvents[eventName] || that.breakpointEvents[eventName])\n debugger; // allow-debugger\n return realEventHandler.apply(that, arguments);\n };\n this.runSoon(this, eventHandler, args);\n }\n }\n \n if (typeof this[LISTENERS][true] !== \"undefined\")\n {\n for (i=0; i < this[LISTENERS][true].length; i++)\n this.runSoon(this, this[LISTENERS][true][i], args.concat(eventName));\n }\n }\n\n /** Emit an event. \n *\n * @param {string} eventName The event to emit\n * @param ... Any other arguments passed will be passed exactly to the listener.\n * @access public\n */\n emit(eventName /* ... */) {\n var args;\n\n args = Array.prototype.slice.call(arguments);\n args.shift();\n\n return this.vemit(eventName, args);\n }\n\n /** Proxy all of the events from another EventEmitter or DOM EventTarget and \n * re-emit them as though they originated from this EventEmitter.\n * \n * @param element The EventEmitter / EventTarget whose events we are to re-emit\n * @param eventName ... One or more arguments which are strings that contain the names of\n * the events to re-emit.\n */\n proxy(element, eventName /*, eventName... */) {\n var i, fun;\n var _this = this;\n\n for (i=1; i < arguments.length; i++) {\n fun = (function events$$EventEmitter$proxy$fun(thisEventName) {\n sanitizeEventName(thisEventName);\n return (function events$$EventEmitter$proxy$fun$bound() {\n if (typeof arguments[0] === 'object')\n arguments[0].proxyType = thisEventName;\n this.vemit(thisEventName, arguments) \n }).bind(_this);\n })(arguments[i]);\n element.addEventListener(arguments[i], fun);\n }\n }\n\n /** Proxy an events from another EventEmitter or DOM EventTarget and \n * re-emit them as though they originated from this EventEmitter, under a new event type.\n * \n * @param element The EventEmitter / EventTarget whose events we are to re-emit\n * @param captureEventName The name of the event to re-emit, or true to re-emit all events\n * @param newEventName The event type of the new event when eventName is not true; when\n * eventName is true, it is used as a prefix prepended to the real\n * event name (ev.type)\n *\n * *note* - Catch-all proxies are only supported on instances of events$$EventEmitter. That\n * functionality is not supported by DOM events.\n */\n proxyAs(element, captureEventName, emitterEventName) {\n var fun;\n captureEventName = sanitizeEventName(captureEventName);\n emitterEventName = sanitizeEventName(emitterEventName);\n \n if (element === this && captureEventName === true)\n throw new Error(\"Infinite recursion is not supported\");\n\n fun = (function events$$EventEmitter$proxyAs$eventHandler(ev) {\n var args = Array.prototype.slice.call(arguments);\n\n if (typeof ev === 'object' && ev.hasOwnProperty('type'))\n ev.proxyType = ev.type;\n\n if (captureEventName === true)\n this.vemit(emitterEventName + args.pop(), args);\n else\n this.vemit(emitterEventName, arguments);\n }).bind(this);\n\n element.addEventListener(captureEventName, fun);\n }\n\n /** Run this supplied function ASAP in the reactor loop, with the provided 'this' and arguments.\n * @param _this [optional] An object to use apply as 'this' when invoking fn\n * @param fn The function to invoke\n * @param args Array of arguments passed to fn during invocation\n */\n runSoon(_this, fn, args) {\n var callback;\n\n if (typeof _this === \"function\" && typeof args === 'undefined') {\n args = fn;\n fn = _this;\n _this = null;\n }\n\n if (typeof fn !== \"function\")\n throw new Error(\"fn is not a function\");\n \n if (typeof args === 'object')\n callback = function runSoon_inner() { return fn.apply(_this, args) };\n else\n callback = function runSoon_inner() { return fn.apply(_this, []) };\n \n return this.doCallback(callback);\n }\n\n get doCallback() { \n return typeof setImmediate === 'function' ? setImmediate : (cb) => { setTimeout(cb, 0) };\n }\n\n /** Debug interface - not stable */\n dumpKnownEvents() {\n var eventName, i;\n var list = [];\n\n if (!this[LISTENERS] || !this[LISTENERS].length)\n return list;\n\n for (eventName in this[LISTENERS])\n list.push(eventName);\n \n list.sort();\n for (i=0; i < list.length; i++) {\n if (list[i] == list[i+1])\n list.splice(i+1,1);\n }\n\n return list;\n }\n\n breakOn(eventName)\n {\n if (!debugBuild)\n throw new Error('breakOn feature not available in release builds');\n \n eventName = sanitizeEventName(eventName);\n if (!this.breakpointEvents)\n this.breakpointEvents = {};\n this.breakpointEvents[eventName] = true;\n }\n}\n\nfunction sanitizeEventName(eventName)\n{\n if (!caseSensitiveEventNames)\n eventName = String(eventName).toLowerCase();\n return eventName;\n}\n\nexports.EventEmitter = EventEmitter;\nexports.caseSensitiveEventNames = caseSensitiveEventNames;\nexports.sanitizeEventName = sanitizeEventName;\n\n\n//# sourceURL=webpack://dcp/./src/common/dcp-events/event-emitter.js?");
3938
3955
 
3939
3956
  /***/ }),
3940
3957
 
@@ -4019,14 +4036,24 @@ eval("/**\n * @file hash.js\n * General purpose utility rout
4019
4036
 
4020
4037
  /***/ }),
4021
4038
 
4039
+ /***/ "./src/common/job-requirements-defaults.js":
4040
+ /*!*************************************************!*\
4041
+ !*** ./src/common/job-requirements-defaults.js ***!
4042
+ \*************************************************/
4043
+ /***/ ((__unused_webpack_module, exports) => {
4044
+
4045
+ eval("exports.DEFAULT_REQUIREMENTS = {\n engine: {\n es7: null,\n spidermonkey: null\n },\n environment: {\n webgpu: null,\n offscreenCanvas: null,\n fdlibm: null\n },\n browser: {\n chrome: null\n },\n details: {\n offscreenCanvas: {\n bigTexture4096: null,\n bigTexture8192: null,\n bigTexture16384: null,\n bigTexture32768: null,\n }\n },\n discrete: null,\n useStrict: null,\n};\n\n/* takes a requirements object and trims it in place to remove any properties that are not in the default requirements */\nexports.removeBadRequirements = function removeBadRequirements (testRequirements, defaultRequirements)\n{\n Object.keys(testRequirements).forEach(requirement =>\n {\n if (typeof defaultRequirements[requirement] === 'object' && defaultRequirements[requirement] !== null)\n return removeBadRequirements(testRequirements[requirement], defaultRequirements[requirement]);\n \n else if(typeof defaultRequirements[requirement] === 'undefined' || typeof testRequirements[requirement] !== 'boolean')\n delete testRequirements[requirement];\n });\n};\n\n/* takes a requirements object and builds it into a proper requirement object that can be used by the protocol */\nexports.buildProtocolRequirements = function buildProtocolRequirements (testRequirements, defaultRequirements)\n{\n Object.keys(testRequirements).forEach(requirement =>\n {\n if (typeof defaultRequirements[requirement] === 'object' && defaultRequirements[requirement] !== null)\n return buildProtocolRequirements(testRequirements[requirement], defaultRequirements[requirement]);\n \n else if(typeof defaultRequirements[requirement] !== 'undefined' && typeof testRequirements[requirement] === 'boolean')\n defaultRequirements[requirement] = testRequirements[requirement];\n });\n};\n\n//# sourceURL=webpack://dcp/./src/common/job-requirements-defaults.js?");
4046
+
4047
+ /***/ }),
4048
+
4022
4049
  /***/ "./src/common/scheduler-constants.js":
4023
4050
  /*!*******************************************!*\
4024
4051
  !*** ./src/common/scheduler-constants.js ***!
4025
4052
  \*******************************************/
4026
- /***/ ((__unused_webpack_module, exports) => {
4053
+ /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
4027
4054
 
4028
4055
  "use strict";
4029
- eval("/**\n * @file scheduler-constants.js\n * Contants and constant-like precomputed values for use with/by DCPv4.\n * All values in this module are completely deterministic and will change\n * only if the source code changes.\n * @author Wes Garland, wes@kingsds.network\n * @date Nov 2020\n */\n\n\n/** Pre-defined, hard-coded compute groups */\nexports.computeGroups = {\n public: {\n opaqueId: 'WHhetL7mj1w1mw1XV6dxyC', \n id: 1,\n name: 'Public Compute Group (open access)',\n joinKey: 'public',\n joinSecret: '',\n },\n};\n\n/**\n * The list of all possible job status in the status column of the jobs table.\n */\nexports.jobStatus = new SchedulerConstantGroup(\n 'cancelled',\n 'corrupted',\n 'estimation',\n 'finished',\n 'running',\n 'paused',\n);\n\n/**\n * The list of all possible slice status in the status column of the\n * activeSlices table.\n */\nexports.sliceStatus = new SchedulerConstantGroup(\n 'overdue',\n 'tiebreaker',\n 'scheduled',\n 'working',\n 'paused',\n 'returned',\n 'new',\n);\n\n/** Currently used bit masks for flags column of jobs table. There is capacity for 31 bit masks. */\nexports.jobFlags = {\n localExec: 1 << 0, /* local exec job; prevent from joining compute groups */\n open: 1 << 1, /* job still open, i.e. able to add more slices */\n workerConsole: 1 << 2, /* job is allowed to log to worker's console - if worker permits also */\n greedyEstimation: 1 << 3, /* Allows job in estimation to have requestTask return multiple estimation slices. */\n isCI: 1 << 4, /* CI job: prevent their slices to be distributed and sheepdog cancel the job*/\n force100pctCPUDensity: 1 << 29, /* Temporary flag that considers the wall clock vs cpu time */\n};\n\nfunction SchedulerConstantGroup()\n{\n var argv = Array.from(arguments);\n var dcpConfig = globalThis.hasOwnProperty('dcpConfig') ? globalThis.dcpConfig : {};\n\n for (let el of argv)\n this[el] = el;\n\n if (dcpConfig.build === 'debug')\n {\n let px = new Proxy(this, {\n get: (target, prop) => {\n if (!target.hasOwnProperty(prop))\n throw new Error(`no such constant: '${prop}'`);\n return target[prop];\n },\n set: (target, prop) => {\n throw new Error('constants are immutable!');\n },\n delete: (prop) => {\n throw new Error('constant groups are immutable!');\n },\n });\n\n return px;\n }\n}\n\nexports.workerIdLength = 22;\n\n\n/** Temporary arrays for backwards compatibility - do not use for new code */\nexports.sliceStatuses = Object.keys(exports.sliceStatus);\nexports.jobStatuses = Object.keys(exports.jobStatus);\n\n\n//# sourceURL=webpack://dcp/./src/common/scheduler-constants.js?");
4056
+ eval("/* provided dependency */ var process = __webpack_require__(/*! ./node_modules/process/browser.js */ \"./node_modules/process/browser.js\");\n/**\n * @file scheduler-constants.js\n * Contants and constant-like precomputed values for use with/by DCPv4.\n * All values in this module are completely deterministic and will change\n * only if the source code changes.\n * @author Wes Garland, wes@kingsds.network\n * @date Nov 2020\n */\n\n\n/** Pre-defined, hard-coded compute groups */\nexports.computeGroups = {\n public: {\n opaqueId: 'WHhetL7mj1w1mw1XV6dxyC', \n id: 1,\n name: 'Public Compute Group (open access)',\n joinKey: 'public',\n joinSecret: '',\n },\n};\n\n/** Currently used bit masks for flags column of jobs table. There is capacity for 31 bit masks. */\nexports.jobFlags = {\n localExec: 1 << 0, /* local exec job; prevent from joining compute groups */\n open: 1 << 1, /* job still open, i.e. able to add more slices */\n workerConsole: 1 << 2, /* job is allowed to log to worker's console - if worker permits also */\n greedyEstimation: 1 << 3, /* Allows job in estimation to have requestTask return multiple estimation slices. */\n isCI: 1 << 4, /* CI job: prevent their slices to be distributed and sheepdog cancel the job*/\n force100pctCPUDensity: 1 << 29, /* Temporary flag that considers the wall clock vs cpu time */\n};\n\nexports.workerIdLength = 22;\n\n/** \n * Constant Groups have a load-time side effect of needing dcpConfig already loaded, in order to\n * determine how they are constructed; proxies are relatively heavy and we don't want them running\n * on production builds if possible.\n */\nfunction initConstantGroups(build)\n{\n clearTimeout(initConstantGroups.timerHnd);\n var argv = Array.from(arguments);\n var dcpConfig = globalThis.hasOwnProperty('dcpConfig') ? globalThis.dcpConfig : {};\n\n /**\n * The list of all possible job status in the status column of the jobs table.\n */\n exports.jobStatus = new SchedulerConstantGroup(\n 'cancelled',\n 'corrupted',\n 'estimation',\n 'finished',\n 'running',\n 'new',\n );\n\n exports.jobValueKind = new SchedulerConstantGroup(\n 'results',\n 'sliceData',\n 'jobArguments',\n );\n\n /**\n * The list of all possible slice status in the status column of the\n * activeSlices table.\n */\n exports.sliceStatus = new SchedulerConstantGroup(\n 'overdue',\n 'tiebreaker',\n 'scheduled',\n 'working',\n 'paused',\n 'returned',\n 'new',\n );\n\n function SchedulerConstantGroup()\n {\n var argv = Array.from(arguments);\n \n for (let el of argv)\n this[el] = el;\n\n if (build === 'debug')\n {\n let px = new Proxy(this, {\n get: (target, prop) => {\n if (!target.hasOwnProperty(prop))\n throw new Error(`no such constant: '${prop}'`);\n return target[prop];\n },\n set: (target, prop) => {\n throw new Error('constants are immutable!');\n },\n delete: (prop) => {\n throw new Error('constant groups are immutable!');\n },\n });\n\n return px;\n }\n }\n\n /** Temporary arrays for backwards compatibility - do not use for new code */\n exports.sliceStatuses = Object.keys(exports.sliceStatus);\n exports.jobStatuses = Object.keys(exports.jobStatus);\n}\n\ninitConstantGroups('release');\n\nsetImmediate(function initConstantGroupsInDebugMode() {\n try\n {\n /* Init the consts groups in debug mode only once dcpConfig is loaded. We can use \"node-only code\" \n * here, because we know that the browser initializes dcpConfig before the bundle is evaluated.\n *\n * We run one tick down the event loop because of dcp-client init semantics.\n */\n if (typeof dcpConfig !== 'undefined' && dcpConfig.build)\n initConstantGroups(dcpConfig.build);\n else\n {\n const { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\n requireNative('dcp/node-libs/config').on('loaded', () => initConstantGroups(dcpConfig.build));\n\n initConstantGroups.timerHnd = setTimeout(() => console.warn('scheduler-constants not fully initialized; please load dcpConfig!'), 10000);\n initConstantGroups.timerHnd.unref();\n }\n }\n catch(error)\n {\n if ( false\n || (process && process.env && process.env.DCP_DEBUG)\n || (dcpConfig && dcpConfig.build === 'debug'))\n console.error('Cannot put scheduler constants into debug mode:', error);\n }\n});\n\n\n//# sourceURL=webpack://dcp/./src/common/scheduler-constants.js?");
4030
4057
 
4031
4058
  /***/ }),
4032
4059
 
@@ -4126,7 +4153,13 @@ eval("/**\n * @file password.js\n * Modal providing a way to
4126
4153
  \**********************************************/
4127
4154
  /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
4128
4155
 
4129
- eval("/**\n * @file client-modal/utils.js\n * @author KC Erb\n * @date Mar 2020\n * \n * All shared functions among the modals.\n */\nconst { fetchRelative } = __webpack_require__(/*! ./fetch-relative */ \"./src/dcp-client/client-modal/fetch-relative.js\");\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\nconst DCP_ENV = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\nexports.OnCloseErrorCode = 'DCP_CM:CANCELX';\n\nif (DCP_ENV.isBrowserPlatform) {\n // Provide as export for the convenience of `utils.MicroModal` instead of a separate require.\n exports.MicroModal = __webpack_require__(/*! micromodal */ \"./node_modules/micromodal/dist/micromodal.es.js\")[\"default\"];\n}\n\n/**\n * Return a unique string, formatted as a GET parameter, that changes often enough to\n * always force the browser to fetch the latest version of our resource.\n *\n * @note Currently always returns the Date-based poison due to webpack. \n */\nfunction cachePoison() {\n if (true)\n return '?ucp=b5864d51c1e7a9b3024698306574b60003a91d62'; /* installer token */\n return '?ucp=' + Date.now();\n}\n \n/* Detect load type - on webpack, load dynamic content relative to webpack bundle;\n * otherwise load relative to the current scheduler's configured portal.\n */\nexports.myScript = (typeof document !== 'undefined') && document.currentScript;\nexports.corsProxyHref = undefined;\nif (exports.myScript && exports.myScript === (__webpack_require__(/*! ./fetch-relative */ \"./src/dcp-client/client-modal/fetch-relative.js\").myScript)) {\n let url = new ((__webpack_require__(/*! dcp/common/dcp-url */ \"./src/common/dcp-url.js\").DcpURL))(exports.myScript.src);\n exports.corsProxyHref = url.resolve('../cors-proxy.html');\n}\n\n/**\n * Look for modal id and required ids on page based on config, if not found, provide from dcp-client.\n * The first id in the required array must be the id of the modal's form element.\n * @param {Object} modalConfig Modal configuration object\n * @param {string} modalConfig.id Id of parent modal element\n * @param {string[]} modalConfig.required Array of required ids in parent modal element\n * @param {string[]} [modalConfig.optional] Array of optional ids in parent modal element\n * @param {string} modalConfig.path Relative path to modal html in dcp-client\n * @returns {DOMElement[]} Array of modal elements on page [config.id, ...config.required]\n */\nexports.initModal = async function (modalConfig, onClose) {\n exports.corsProxyHref = exports.corsProxyHref || dcpConfig.portal.location.resolve('dcp-client/cors-proxy.html');\n\n // Call ensure modal on any eager-loaded modals.\n if (modalConfig.eagerLoad) {\n Promise.all(\n modalConfig.eagerLoad.map(config => ensureModal(config))\n )\n };\n\n const [elements, optionalElements] = await ensureModal(modalConfig);\n\n // Wire up form to prevent default, resolve on submission, reject+reset when closed (or call onClose when closed)\n const [modal, form] = elements;\n form.reset(); // ensure that form is fresh\n let formResolve, formReject;\n let formPromise = new Promise( function(res, rej) {\n formResolve = res;\n formReject = rej;\n });\n form.onsubmit = function (submitEvent) {\n submitEvent.preventDefault();\n modal.setAttribute(\"data-state\", \"submitted\");\n formResolve(submitEvent);\n }\n\n exports.MicroModal.show(modalConfig.id, { \n disableFocus: true, \n onClose: onClose || getDefaultOnClose(formReject)\n });\n return [elements, formPromise, optionalElements];\n};\n\n// Ensure all required modal elements are on page according to modalConfig\nasync function ensureModal(modalConfig) {\n let allRequiredIds = [modalConfig.id, ...modalConfig.required];\n let missing = allRequiredIds.filter( id => !document.getElementById(id) );\n if (missing.length > 0) {\n if (missing.length !== allRequiredIds.length)\n console.warn(`Some of the ids needed to replace the default DCP-modal were found, but not all. So the default DCP-Modal will be used. Missing ids are: [${missing}].`);\n let contents = await fetchRelative(exports.corsProxyHref, modalConfig.path + cachePoison());\n const container = document.createElement('div');\n container.innerHTML = contents;\n document.body.appendChild(container);\n }\n\n const elements = allRequiredIds.map(id => document.getElementById(id));\n const optionalElements = (modalConfig.optional || []).map(id => document.getElementById(id));\n return [elements, optionalElements];\n};\n\n// This onClose is called by MicroModal and thus has the modal passed to it.\nfunction getDefaultOnClose (formReject) {\n return (modal) => {\n modal.offsetLeft; // forces style recalc\n const origState = modal.dataset.state;\n // reset form including data-state\n modal.setAttribute(\"data-state\", \"new\");\n // reject if closed without submitting form.\n if (origState !== \"submitted\") {\n const err = new DCPError(\"Modal was closed but modal's form was not submitted.\", exports.OnCloseErrorCode);\n formReject(err);\n }\n }\n}\n\n\n//# sourceURL=webpack://dcp/./src/dcp-client/client-modal/utils.js?");
4156
+ <<<<<<< HEAD
4157
+ 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=4dd484422936278679ed70ce7501d11964b9d792'; /* 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?");
4158
+ ||||||| e0e85dd
4159
+ 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=057ccd920bfd464401288eed9b98f438ace69b2e'; /* 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?");
4160
+ =======
4161
+ 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=d4f755b478f851a75b532e258d99ea2c45d288fb'; /* 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?");
4162
+ >>>>>>> origin/prod-20220907
4130
4163
 
4131
4164
  /***/ }),
4132
4165
 
@@ -4137,7 +4170,7 @@ eval("/**\n * @file client-modal/utils.js\n * @author KC Erb\n * @date Mar 2020\
4137
4170
  /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
4138
4171
 
4139
4172
  "use strict";
4140
- eval("/**\n * @file Client facing module that implements Compute Groups API\n * @module dcp/compute-groups\n * @access public\n * @author Kayra E-A <kayra@kingsds.network>\n * Wes Garland <wes@kingsds.network>\n * Paul <paul@kingsds.network>\n * @date Sept 2020\n * February 2022\n * May 2022\n */\n\n\nconst protocolV4 = __webpack_require__(/*! dcp/protocol-v4 */ \"./src/protocol-v4/index.js\");\nconst wallet = __webpack_require__(/*! dcp/dcp-client/wallet */ \"./src/dcp-client/wallet/index.js\");\nconst hash = __webpack_require__(/*! ../../common/hash */ \"./src/common/hash.js\");\nconst { DCPError } = __webpack_require__(/*! ../../common/dcp-error */ \"./src/common/dcp-error.js\");\nconst debugging = (__webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope)('scheduler');\nconst { Address } = __webpack_require__(/*! dcp/dcp-client/wallet */ \"./src/dcp-client/wallet/index.js\");\nconst constants = __webpack_require__(/*! dcp/common/scheduler-constants */ \"./src/common/scheduler-constants.js\");\nconst { clientError, reconstructServiceError } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\n\n/** @typedef {import('dcp/utils').apiServiceType} apiServiceType */\n/** @typedef {import('dcp/utils').apiClientType} apiClientType */\n/** @typedef {string} opaqueId */\n\n/**\n * @typedef {object} cgAccessType\n * @property {opaqueId} [id]\n * @property {string} [joinKey]\n */\n\n/**\n * @typedef {object} cgClientJoinType\n * @property {opaqueId} [id]\n * @property {Address} [joinAddress]\n * @property {string} [joinKey]\n * @property {string} [joinSecret]\n * @property {string} [joinHash]\n */\n\n/**\n * @typedef {object} cgServiceJoinType\n * @property {opaqueId} [id]\n * @property {Address} [joinAddress]\n * @property {string} [joinKey]\n * @property {string} [joinHashHash]\n */\n\n/**\n * Establishes the client connection to the computeGroups microservice if it does not exist already from the default config.\n * \n * @returns {protocolV4.Connection}\n * @access public\n * @example\n * const result = await exports.serviceConnection.send('createGroup', {\n name: name,\n description: description,\n });\n */\n\nexports.serviceConnection = null;\n\n//\n// Reference counting pattern:\n// For every time addRef is called,\n// closeServiceConnection must eventually be called.\n// Reference counting allows multiple execs in a Promise.all .\n//\nvar refCount = 0;\nexports.addRef = function addRef() {\n refCount++;\n}\n\nconst openAndConnectServiceConn = async function openAndConnectServiceConn()\n{\n exports.serviceConnection = new protocolV4.Connection(dcpConfig.scheduler.services.computeGroups);\n exports.serviceConnection.on('close', openAndConnectServiceConn);\n await exports.serviceConnection.connect();\n refCount = 0; // Help with sanity.\n}\n\n/**\n * Resets the client connection to the computeGroups microservice.\n */\nexports.closeServiceConnection = async function closeServiceConnection() {\n if (refCount > 0) refCount--;\n if (exports.serviceConnection && refCount < 1)\n {\n exports.serviceConnection.off('close', openAndConnectServiceConn);\n exports.serviceConnection.close(null, true);\n refCount = 0; // Help with sanity.\n exports.serviceConnection = null;\n }\n};\n\n/**\n * (Used in jobs/index.js)\n * KeepAlive for the service connection to compute groups.\n */\nexports.keepAlive = async function keepAlive() {\n if (!exports.serviceConnection)\n await openAndConnectServiceConn();\n\n exports.serviceConnection.keepalive().catch(err => console.error('Warning: keepalive failed for compute groups service', err));\n}\n\n/**\n * Checks whether descriptor corresponds to the public compute group from the scheduler constants.\n */\nexports.isPublicComputeGroup = function isPublicComputeGroup(descriptor) {\n return descriptor.id === constants.computeGroups.public.id\n && descriptor.opaqueId === constants.computeGroups.public.opaqueId;\n};\n\n/**\n * Returns a compute group identification snippet for diagnostic messages,\n * @param {object} descriptor - Must have one of the properties joinKey, id (id:=opaqueId). Specifically\n * descriptor = { joinKey: 'dcpDemo' } or descriptor = { id: 'bYcYGQ3NOpFnP4FKs6IBQd' },\n * where the corresponding row in table computeGroups have attributes\n * joinKey:='dcpDemo' or opaqueId:='bYcYGQ3NOpFnP4FKs6IBQd' .\n * @returns {string}\n */\nfunction cgId(descriptor) {\n return (descriptor.joinKey) ? `joinKey ${descriptor.joinKey}` : `id ${descriptor.id}`;\n}\n\n/**\n * Verify sufficient information in descriptor to access a compute group.\n * Emit diagnostics about unnecessary information.\n * @param {cgAccessType} descriptor \n * @param {string} methodName \n */\nfunction validateCGDescriptor(descriptor, methodName) {\n for (const prop in descriptor) {\n if ([ 'id', 'joinKey' ].includes(prop)) continue;\n if ([ 'joinAddress', 'joinHash', 'joinSecret' ].includes(prop))\n console.warn(`It is not necessary to specify '${prop}' in the descriptor ${JSON.stringify(descriptor)} when calling ${methodName}`);\n else\n console.error(`Do not specify '${prop}' in the descriptor ${JSON.stringify(descriptor)} when calling ${methodName}`);\n }\n}\n\n/**\n * Verify sufficient information in descriptor to authorize a compute group.\n * Emit diagnostics about unnecessary information.\n * @param {cgClientJoinType} joinDescriptor \n * @param {string} methodName \n */\nfunction validateCGJoinDescriptor(joinDescriptor, methodName) {\n for (const prop in joinDescriptor) {\n if ([ 'id', 'joinKey', 'joinSecret', 'joinHash', 'joinAddress' ].includes(prop)) continue;\n console.error(`Do not specify '${prop}' in the descriptor ${JSON.stringify(joinDescriptor)} when calling ${methodName}`);\n }\n}\n\n/**\n * Build message to go across the wire.\n * Verify sufficient information in descriptor to access a compute group.\n * Emit diagnostics about unnecessary information.\n * @param {cgAccessType} descriptor\n * @param {string} methodName\n * @returns {cgAccessType}\n */\nfunction buildCGMessage(descriptor, methodName)\n{\n if (exports.isPublicComputeGroup(descriptor)) return descriptor;\n\n const message = {};\n // Construct message.joinKey xor message.id .\n if (descriptor.joinKey) message.joinKey = descriptor.joinKey;\n else if (descriptor.id) message.id = descriptor.id; // id:=opaqueId\n\n debugging('computeGroups') && console.debug(`${methodName}:buildCGMessage: descriptor`, descriptor, 'message', message);\n\n validateCGDescriptor(descriptor, methodName);\n\n return message;\n}\n\n/**\n * Build message so that joinHash, joinSecret, opaqueId do not go across the wire.\n * Verify sufficient information in descriptor to authorize a compute group.\n * Emit diagnostics about unnecessary information.\n * @param {cgClientJoinType} descriptor\n * @param {string} methodName\n * @returns {cgServiceJoinType}\n */\nfunction buildCGJoinMessage(descriptor, methodName)\n{\n if (exports.isPublicComputeGroup(descriptor)) return descriptor;\n\n const message = {};\n // Construct message.joinKey xor message.id .\n if (descriptor.joinKey) message.joinKey = descriptor.joinKey;\n else if (descriptor.id) message.id = descriptor.id; // id:=opaqueId\n // Construct message.joinAddress .\n if (descriptor.joinAddress) message.joinAddress = descriptor.joinAddress;\n\n debugging('computeGroups') && console.debug(`${methodName}:buildCGJoinMessage: descriptor`, descriptor, 'message', message);\n\n validateCGJoinDescriptor(descriptor, methodName);\n\n // Construct message.joinHashHash .\n if (descriptor.joinSecret) message.joinHashHash = hash.calculate(hash.eh1, exports.calculateJoinHash(descriptor), exports.serviceConnection.dcpsid);\n if (descriptor.joinHash) message.joinHashHash = hash.calculate(hash.eh1, descriptor.joinHash, exports.serviceConnection.dcpsid);\n\n return message;\n}\n\nfunction hasSufficientJoinInfo(joinDescriptor) {\n // Verify joinDescriptor has sufficient information to authorize a compute group (not guarenteed).\n return (joinDescriptor.joinKey && (joinDescriptor.joinSecret || joinDescriptor.joinHash))\n || (joinDescriptor.id && joinDescriptor.joinAddress)\n || exports.isPublicComputeGroup(joinDescriptor);\n}\n\nconst newCGPrototype = { type: 'object',\n parameters: {\n // name: { type: 'string', default: undefined }, /* name of group (length <= 255) */\n // description: { type: 'string', default: undefined }, /* description of group (length <= 255) */\n // id: { type: 'string', default: undefined }, /* opaqueId, the unique identifier of the compute group; nanoid (length === 22) */\n // joinKey: { type: 'string', default: undefined }, /* basically the login (length <= 255) */\n // joinSecret: { type: 'string', default: undefined }, /* basically the password (length <= 255) */\n // joinHash: { type: 'string', default: undefined }, /* basically the password, the joinSecret seeded & hashed */\n // joinAddress: { type: Address, default: undefined }, /* signature gives alternative to login/password */\n\n commissionRate: { type: 'BigNumber', default: undefined }, /* commission, see DCP-1889 */\n deployFee: { type: 'BigNumber', default: undefined }, /* number of DCC to take for every deployment */\n deployAccess: { type: 'string', default: undefined }, /* can be \"owner\"|\"join\" (dcp-1910) */\n addJobFee: { type: 'BigNumber', default: undefined }, /* fee required each time a job joins a compute group */\n maxTotalPayment: { type: 'BigNumber', default: undefined }, /* limit on maximum job payment, NULL => Infinity */\n\n /* Administrative limits on group. NULL => Infinity: Should all be integers or undefined. */\n maxConcurrentJobs: { type: 'number', default: undefined },\n maxConcurrentWorkers: { type: 'number', default: undefined },\n maxConcurrentSandboxes: { type: 'number', default: undefined },\n maxConcurrentCPUs: { type: 'number', default: undefined },\n maxConcurrentGPUs: { type: 'number', default: undefined },\n maxConcurrentEscrow: { type: 'BigNumber', default: undefined },\n },\n};\n\n/**\n * Async function that creates a new Compute Group.\n *\n * The joinDescriptor is of the form { joinKey, joinSecret }, { joinKey, joinHash } or { id, joinAddress }.\n * where id will correspond to the attribute opaqueId in the new row in the computeGroups table.\n *\n * This function can only be called with ADMIN permission.\n * Properties not appearing in newCGPrototype.parameters are not allowed in otherProperties.\n *\n * @param {cgClientJoinType} joinDescriptor - Must have properly defined { joinKey, joinSecret }, { joinKey, joinHash }\n * or { id, joinAddress }, where id will correspond to the attribute opaqueId\n * in the new row in the computeGroups table.\n * @param {string} [name] - The name of the compute group.\n * @param {string} [description] - The description of the compute group.\n * @param {object} [otherProperties] - The 5 attributes of table computeGroup related to commissions and fees.\n * commissionRate: notNull(zDec18), // commission, see DCP-1889\n * deployFee: notNull(zDec18), // number of DCC to take for every deployment\n * deployAccess: string, // can be \"owner\"|\"join\" (dcp-1910)\n * addJobFee: notNull(zDec18), // fee required each time a job joins a compute group\n * maxTotalPayment: dec18, // limit on maximum job payment, NULL => Infinity\n * And the 6 attributes of table computeGroup related to limits.\n * maxConcurrentJobs: integer,\n * maxConcurrentWorkers: integer,\n * maxConcurrentSandboxes: integer,\n * maxConcurrentCPUs: integer,\n * maxConcurrentGPUs: integer,\n * maxConcurrentEscrow: dec18,\n * @returns {Promise<apiClientType>} - { success, payload: computeGroup.id }\n * @access public\n * @example\n * await computeGroup.createGroup({ joinKey: 'dcpDemo', joinSecret: 'theSecret' }, 'myCGName', 'myCGDescription', { deployFee: 0.00015 });\n * await computeGroup.createGroup({ joinKey: 'dcpDemo2', joinHash: 'eh1-e063976b20a92da97a27b9873465c6f2c9d6e4370befa86c8c1dd312c78befc2' });\n * await computeGroup.createGroup({ id: 'bYcYGQ3NOpFnP4FKs6IBQd', joinAddress: 'c15053fc30d4bdf91e2e0bba79578f8b649e55ea' });\n * where the corresponding rows in table computeGroups have attributes\n * row1: joinKey:='dcpDemo',\n * row2: joinKey:='dcpDemo2', joinHash:='eh1-e063976b20a92da97a27b9873465c6f2c9d6e4370befa86c8c1dd312c78befc2'\n * row3: opaqueId:='bYcYGQ3NOpFnP4FKs6IBQd', joinAddress:='c15053fc30d4bdf91e2e0bba79578f8b649e55ea' .\n */\nexports.createGroup = async function createGroup(joinDescriptor, name, description, otherProperties)\n{\n if (!exports.serviceConnection)\n await openAndConnectServiceConn();\n\n validateCGJoinDescriptor(joinDescriptor, 'createGroup');\n\n // Verify joinDescriptor has sufficient information to authorize a compute group (not guarenteed).\n if (!hasSufficientJoinInfo(joinDescriptor))\n return clientError(`createGroup: Insufficient information to authorize compute group: ${JSON.stringify(joinDescriptor)}.`);\n\n // Validate the properties in otherProperties.\n for (const methodName in otherProperties) {\n if (!Object.keys(newCGPrototype.parameters).includes(methodName))\n return clientError(`createGroup: Property ${methodName} cannot be speicfied in otherProperties. Can only specify ${JSON.stringify(Object.keys(newCGPrototype.parameters))}`);\n }\n\n // Translate joinSecret to joinHash.\n if (joinDescriptor.joinSecret) {\n joinDescriptor.joinHash = exports.calculateJoinHash(joinDescriptor);\n delete joinDescriptor.joinSecret;\n }\n\n if (otherProperties && (otherProperties.commissionRate < 0 || otherProperties.commissionRate >= 1))\n return clientError(`client-createGroup: commissionRate ${otherProperties.commissionRate} must be between 0 and 1 (0 <= commissionRate < 1).`);\n\n debugging('computeGroups') && console.debug('client-createGroup: input:', joinDescriptor, name, description, otherProperties);\n\n const { success, payload } = await exports.serviceConnection.send('createGroup', { joinDescriptor, name, description, otherProperties });\n\n if (!success) return clientError(`Cannot create new compute group, with ${cgId(joinDescriptor)}.`);\n if (payload && !payload.success) return reconstructServiceError(payload);\n\n debugging('computeGroups') && console.debug('client-createGroup: payload', payload);\n\n return payload;\n};\n\nconst changeCGPrototype = { type: 'object',\n parameters: {\n name: { type: 'string', default: undefined }, /* name of group (length <= 255) */\n description: { type: 'string', default: undefined }, /* description of group (length <= 255) */\n joinHash: { type: 'string', default: undefined }, /* basically the password, seeded & hashed (length <= 255) */\n joinAddress: { type: Address, default: undefined }, /* signature gives alternative to login/password */\n\n commissionRate: { type: 'BigNumber', default: undefined }, /* commission, see DCP-1889 */\n deployFee: { type: 'BigNumber', default: undefined }, /* number of DCC to take for every deployment */\n deployAccess: { type: 'string', default: undefined }, /* can be \"owner\"|\"join\" (dcp-1910) */\n addJobFee: { type: 'BigNumber', default: undefined }, /* fee required each time a job joins a compute group */\n maxTotalPayment: { type: 'BigNumber', default: undefined }, /* limit on maximum job payment, NULL => Infinity */\n\n /* Administrative limits on group. NULL => Infinity: Should all be integers or undefined. */\n maxConcurrentJobs: { type: 'number', default: undefined },\n maxConcurrentWorkers: { type: 'number', default: undefined },\n maxConcurrentSandboxes: { type: 'number', default: undefined },\n maxConcurrentCPUs: { type: 'number', default: undefined },\n maxConcurrentGPUs: { type: 'number', default: undefined },\n maxConcurrentEscrow: { type: 'BigNumber', default: undefined },\n },\n};\n\n/**\n * Async function that changes a new Compute Group.\n * \n * The parameter newDescriptor contains the new property values,\n * and the properties that are allowed to be changed appear in changeCGPrototype.parameters.\n * \n * The descriptor must have joinKey or id, where id:=opaqueId.\n * Must own the compute group or be ADMIN to use changeGroup.\n * \n * @param {cgAccessType} descriptor - Must have joinkey or id, where id:=opaqueId.\n * @param {object} newDescriptor - Properties not appearing in changeCGPrototype.parameters are not allowed.\n * @returns {Promise<apiClientType>}\n * await computeGroup.changeGroup({ joinKey: 'dcpDemo' }, { joinSecret: 'myNewPasswrd' });\n * await computeGroup.changeGroup({ id: 'bYcYGQ3NOpFnP4FKs6IBQd' }, { name: 'myNewName', deployFee: 0.0001 });\n * where the corresponding rows in table computeGroups have attributes\n * row1: joinKey:='dcpDemo',\n * row2: opaqueId:='bYcYGQ3NOpFnP4FKs6IBQd'\n */\nexports.changeGroup = async function changeGroup(descriptor, newDescriptor)\n{\n if (!exports.serviceConnection)\n await openAndConnectServiceConn();\n\n // Verify descriptor has sufficient information to access a compute group (not guarenteed).\n if (!descriptor.joinKey && !descriptor.id)\n return clientError(`changeGroup: Insufficient information to identify compute group: ${JSON.stringify(descriptor)}.`);\n\n // Validate the properties in newDescriptor.\n for (const methodName in newDescriptor) {\n if (!Object.keys(changeCGPrototype.parameters).includes(methodName))\n return clientError(`changeGroup: Not allowed to change property ${methodName}. Can only change ${JSON.stringify(Object.keys(changeCGPrototype.parameters))}`);\n }\n\n // Translate joinSecret to joinHash.\n if (newDescriptor.joinSecret) {\n newDescriptor.joinHash = exports.calculateJoinHash(newDescriptor);\n delete newDescriptor.joinSecret;\n }\n\n descriptor = buildCGMessage(descriptor, 'changeGroup');\n debugging('computeGroups') && console.debug('change compute group client:', descriptor, newDescriptor);\n const { success, payload } = await exports.serviceConnection.send('changeGroup', { descriptor, newDescriptor });\n\n if (!success) throw new DCPError(`Cannot change compute group with ${cgId(descriptor)}:`, payload);\n if (payload && !payload.success) return reconstructServiceError(payload);\n\n return payload;\n};\n\n/**\n * Async function that deletes a compute group.\n * \n * The descriptor must have joinkey or id, where id:=opaqueId.\n * \n * Must either own the group or be ADMIN.\n * If not ADMIN, then the following config must be true:\n * dcpConfig.scheduler.services.computeGroups.usersCanDeleteGroups\n * \n * @param {cgAccessType} descriptor - Must contain joinKey or id (id:=opaqueId) \n * @returns {Promise<apiClientType>}\n * await computeGroup.deleteGroup({ joinKey: 'dcpDemo' });\n * await computeGroup.deleteGroup({ id: 'bYcYGQ3NOpFnP4FKs6IBQd' });\n * where the corresponding rows in table computeGroups have attributes\n * row1: joinKey:='dcpDemo',\n * row2: opaqueId:='bYcYGQ3NOpFnP4FKs6IBQd'\n */\nexports.deleteGroup = async function deleteGroup(descriptor)\n{\n if (!exports.serviceConnection)\n await openAndConnectServiceConn();\n\n // Verify descriptor has sufficient information to access a compute group (not guarenteed).\n if (!descriptor.joinKey && !descriptor.id)\n return clientError(`deleteGroup: Insufficient information to identify compute group: ${JSON.stringify(descriptor)}.`);\n\n descriptor = buildCGMessage(descriptor, 'deleteGroup');\n debugging('computeGroups') && console.debug('delete compute group client:', descriptor);\n const { success, payload } = await exports.serviceConnection.send('deleteGroup', { descriptor });\n\n if (!success) throw new DCPError(`Cannot delete compute group with ${cgId(descriptor)}:`, payload);\n if (payload && !payload.success) return reconstructServiceError(payload);\n\n return payload;\n};\n\n/**\n * Async function that adds a job to a specified compute group. \n * \n * Must be the owner of the job.\n *\n * Useful feedback is provided from this function, as it\n * will make its way back to the application developer, *after* they have made the\n * deployment fee micropayment.\n *\n * On the client side the access model in place is that if you know the (user/password)\n * joinKey+joinSecret/joinKey+joinHash/joinKey+joinHashHash/id+joinAddress,\n * you can add the job to the compute groups, where id:=opaqueId from table computeGroups.\n * On the service side the corresponding access model is\n * joinKey+joinHashHash/id+joinAddress .\n * Access is also allowed if the compute group owner is the connection peerAddress.\n * \n * Unless the compute group owner is the connection peerAddress, element of the descriptor array must contain\n * { joinKey, joinSecret }, { joinKey, joinHash } or { id, joinAddress }\n * where the value of id in { id, joinAddress } is the opaqueId attribute of the row in table computeGroups.\n *\n * @param {Address} job The address of the Job that will be added to the Compute Group.\n * @param {Array} computeGroups Array of descriptor objects for the compute groups. This descriptor\n * needs to contain enough information to authorize access to the\n * compute group. Properties may include:\n * - id (id:=opaqueId)\n * - joinKey\n * - joinSecret\n * - joinHash\n * - joinAddress\n * \n * Additional, either the joinKey or id MUST be specified so\n * that we can identify the compute group in question.\n *\n * All compute groups can have jobs submitted to them, provided either the joinKey\n * or the id are specified, and the message contains valid join permission and the \n * job is owned by the caller of addJobToGroups.\n *\n * FUTURE - after DCP-1910\n * keystore A keystore used to grant access to job deployment within this compute group.\n * This can be either the ownerKeystore or the joinAddress keystore when the\n * compute group is in deployAccessType='join' mode.\n * @returns {Promise<apiClientType>}\n * @access public\n * @example\n * await computeGroup.addJobToGroups('P+Y4IApeFQLrYS2W7MkVg7', \n * [ { joinKey: 'dcpDemo', joinSecret: 'theSecret' },\n * { joinKey: 'dcpDemo2', joinHash: 'eh1-e063976b20a92da97a27b9873465c6f2c9d6e4370befa86c8c1dd312c78befc2' }, \n * { id: 'bYcYGQ3NOpFnP4FKs6IBQd', joinAddress: 'c15053fc30d4bdf91e2e0bba79578f8b649e55ea' } ]);\n * where the corresponding rows in table computeGroups have attributes\n * row1: joinKey:='dcpDemo',\n * row2: joinKey:='dcpDemo2', joinHash:='eh1-e063976b20a92da97a27b9873465c6f2c9d6e4370befa86c8c1dd312c78befc2'\n * row3: opaqueId:='bYcYGQ3NOpFnP4FKs6IBQd', joinAddress:='c15053fc30d4bdf91e2e0bba79578f8b649e55ea' .\n */\nexports.addJobToGroups = async function addJobToGroups(job, computeGroups)\n{\n if (!exports.serviceConnection)\n await openAndConnectServiceConn();\n\n const cgArray = [];\n for (const joinDescriptor of computeGroups)\n {\n // Verify joinDescriptor has sufficient information to authorize a compute group (not guarenteed).\n if (!hasSufficientJoinInfo(joinDescriptor))\n console.error(`addJobToGroups: Insufficient information to authorize compute group: ${JSON.stringify(joinDescriptor)}.`);\n\n // Translate so that neither joinHash nor joinSecret goes across the wire.\n const message = buildCGJoinMessage(joinDescriptor, 'addJobToGroups');\n debugging('computeGroups') && console.debug(`addJobToGroups client: job ${job}, message`, message);\n\n cgArray.push(message);\n }\n\n const { success, payload } = await exports.serviceConnection.send('addJobToGroups', { job, cgArray });\n\n debugging('computeGroups') && console.debug('addJobToGroups payload', payload);\n\n if (!success) throw new DCPError(`Cannot add job ${job} to compute groups.`, payload);\n if (payload && !payload.success) return reconstructServiceError(payload);\n\n // If the server reported success but did not return a list of CGs (eg. v.4.2.5 server),\n // assume (and inform the client) we added all groups successfully\n return payload || computeGroups;\n};\n\n/**\n * Async function that lists all the Jobs in a Compute Group.\n * \n * The descriptor must have one of the properties joinkey, id (id:=opaqueId).\n * Must be the owner of the Compute Group to list jobs from it.\n * The job does not need to be owned.\n * \n * The descriptor is of the form { joinKey: 'dcpDemo' } or { id: 'bYcYGQ3NOpFnP4FKs6IBQd' }.\n * where 'bYcYGQ3NOpFnP4FKs6IBQd' is the opaqueId of the Compute Group.\n *\n * @param {cgAccessType} descriptor - Must have one of the properties joinKey, id (id:=opaqueId). Specifically\n * descriptor = { joinKey: 'dcpDemo' } or descriptor = { id: opaqueId }\n * @returns {Promise<apiClientType>}\n * @access public\n * @example\n * let listOfJobs1 = await computeGroup.listJobs({ joinKey: 'dcpDemo' });\n * let listOfJobs2 = await computeGroup.listJobs({ id: 'bYcYGQ3NOpFnP4FKs6IBQd' });\n * where the corresponding rows in table computeGroups have attributes\n * row1: joinKey:='dcpDemo'\n * row2: opaqueId:='bYcYGQ3NOpFnP4FKs6IBQd'\n */\nexports.listJobs = async function listJobs(descriptor)\n{\n if (!exports.serviceConnection)\n await openAndConnectServiceConn();\n\n // Verify descriptor has sufficient information to access a compute group (not guarenteed).\n if (!descriptor.joinKey && !descriptor.id)\n return clientError(`listJobs: Insufficient information to identify compute group: ${JSON.stringify(descriptor)}.`);\n\n descriptor = buildCGMessage(descriptor, 'listJobs');\n debugging('computeGroups') && console.debug('listJob client: descriptor', descriptor);\n const { success, payload } = await exports.serviceConnection.send('listJobs', { descriptor });\n\n if (!success) throw new DCPError(`Cannot list jobs for compute group with ${cgId(descriptor)}`, payload);\n if (payload && !payload.success) return reconstructServiceError(payload);\n\n return payload;\n};\n\n/**\n * Async function that removes a job from a Compute Group.\n * \n * The descriptor must have one of the properties joinkey, id (id:=opaqueId).\n * Must be the owner of the Compute Group to remove a job from it.\n * The job does not need to be owned.\n * \n * The descriptor is of the form { joinKey: 'dcpDemo' } or { id: 'bYcYGQ3NOpFnP4FKs6IBQd' }.\n * where 'bYcYGQ3NOpFnP4FKs6IBQd' is the opaqueId of the Compute Group.\n *\n * @param {Address} job - The address of the Job that will be added to the Compute Group.\n * @param {cgAccessType} descriptor - { joinKey: 'dcpDemo' } or { id: 'bYcYGQ3NOpFnP4FKs6IBQd' }\n * @returns {Promise<apiClientType>}\n * @access public\n * @example\n * await computeGroup.removeJob( 'P+Y4IApeFQLrYS2W7MkVg7', { joinKey: 'dcpDemo' });\n * await computeGroup.removeJob( 'P+Y4IApeFQLrYS2W7MkVg7', { id: 'bYcYGQ3NOpFnP4FKs6IBQd' });\n * where the corresponding rows in table computeGroups have attributes\n * row1: joinKey:='dcpDemo'\n * row2: opaqueId:='bYcYGQ3NOpFnP4FKs6IBQd'\n */\nexports.removeJob = async function removeJob(job, descriptor)\n{\n if (!exports.serviceConnection)\n await openAndConnectServiceConn();\n\n // Verify descriptor has sufficient information to access a compute group (not guarenteed).\n if (!descriptor.joinKey && !descriptor.id)\n return clientError(`removeJob: Insufficient information to identify compute group: ${JSON.stringify(descriptor)}.`);\n\n descriptor = buildCGMessage(descriptor, 'removeJob');\n debugging('computeGroups') && console.debug(`removeJob client: job ${job}, descriptor`, descriptor);\n const { success, payload } = await exports.serviceConnection.send('removeJob', { job, descriptor });\n\n if (!success) throw new DCPError(`Cannot remove job ${job} from compute group with ${cgId(descriptor)}`, payload);\n if (payload && !payload.success) return reconstructServiceError(payload);\n\n return payload;\n};\n\n/**\n * Async function that removes all jobs from a Compute Group.\n * \n * The descriptor must have one of the properties joinkey, id (id:=opaqueId).\n * Must be the owner of the Compute Group to remove jobs from it.\n * \n * The descriptor is of the form { joinKey: 'dcpDemo' } or { id: 'bYcYGQ3NOpFnP4FKs6IBQd' }.\n * where 'bYcYGQ3NOpFnP4FKs6IBQd' is the opaqueId of the Compute Group.\n *\n * @param {cgAccessType} descriptor - { joinKey: 'dcpDemo' } or { id: 'bYcYGQ3NOpFnP4FKs6IBQd' }\n * @returns {Promise<apiClientType>}\n * @access public\n * @example\n * await computeGroup.removeAllJobs({ joinKey: 'dcpDemo' });\n * await computeGroup.removeAllJobs({ id: 'bYcYGQ3NOpFnP4FKs6IBQd' });\n * where the corresponding rows in table computeGroups have attributes\n * row1: joinKey:='dcpDemo'\n * row2: opaqueId:='bYcYGQ3NOpFnP4FKs6IBQd'\n */\nexports.removeAllJobs = async function removeAllJobs(descriptor)\n{\n if (!exports.serviceConnection)\n await openAndConnectServiceConn();\n\n // Verify descriptor has sufficient information to access a compute group (not guarenteed).\n if (!descriptor.joinKey && !descriptor.id)\n return clientError(`removeAllJobs: Insufficient information to identify compute group: ${JSON.stringify(descriptor)}.`);\n\n descriptor = buildCGMessage(descriptor, 'removeAllJobs');\n debugging('computeGroups') && console.debug('removeAllJobs client: descriptor', descriptor);\n const { success, payload } = await exports.serviceConnection.send('removeAllJobs', { descriptor });\n\n if (!success) throw new DCPError(`Cannot remove all jobs from compute group with ${cgId(descriptor)}:`, payload);\n if (payload && !payload.success) return reconstructServiceError(payload);\n\n return payload;\n};\n\n/**\n * Async function that cancels the specified owned job.\n * \n * Must be the owner of the job.\n *\n * On the client side the access model in place is that if you know the (user/password)\n * joinKey+joinSecret/joinKey+joinHash/joinKey+joinHashHash/id+joinAddress,\n * you can cancel the job in the compute group, where id:=opaqueId from table computeGroups.\n * On the service side the corresponding access model is\n * joinKey+joinHashHash/id+joinAddress .\n * Access is also allowed if the compute group owner is the connection peerAddress.\n * \n * Unless the compute group owner is the connection peerAddress, the descriptor must contain\n * { joinKey, joinHashHash } or { id, joinAddress }\n * where the value of id in { id, joinAddress } is the opaqueId attribute of the row in table computeGroups.\n * \n * @param {Address} job - The address of the Job that will be added to the Compute Group.\n * @param {cgClientJoinType} joinDescriptor - Array of descriptor objects for the compute groups. This descriptor\n * needs to contain enough information to authorize access to the\n * compute group. Properties may include:\n * - id (id:=opaqueId)\n * - joinKey\n * - joinSecret\n * - joinHash\n * - joinAddress\n *\n * Additional, either the joinKey or id MUST be specified so\n * that we can identify the compute group in question.\n * @returns {Promise<apiClientType>}\n * @access public\n * @example\n * await computeGroup.cancelJob( 'P+Y4IApeFQLrYS2W7MkVg7', { joinKey: 'dcpDemo', joinHash: 'eh1-e063976b20a92da97a27b9873465c6f2c9d6e4370befa86c8c1dd312c78befc2' } );\n * await computeGroup.cancelJob( 'P+Y4IApeFQLrYS2W7MkVg7', { id: 'bYcYGQ3NOpFnP4FKs6IBQd', joinAddress: 'c15053fc30d4bdf91e2e0bba79578f8b649e55ea' });\n * where the corresponding rows in table computeGroups have attributes\n * row1: joinKey:='dcpDemo', joinHash:='eh1-e063976b20a92da97a27b9873465c6f2c9d6e4370befa86c8c1dd312c78befc2'\n * row2: opaqueId:='bYcYGQ3NOpFnP4FKs6IBQd', joinAddress:='c15053fc30d4bdf91e2e0bba79578f8b649e55ea' .\n */\nexports.cancelJob = async function cancelJob(job, joinDescriptor)\n{\n if (!exports.serviceConnection)\n await openAndConnectServiceConn();\n\n // Verify joinDescriptor has sufficient information to authorize a compute group (not guarenteed).\n if (!hasSufficientJoinInfo(joinDescriptor))\n return clientError(`cancelJob: Insufficient information to authorize compute group: ${JSON.stringify(joinDescriptor)}.`);\n\n // Translate so that neither joinHash nor joinSecret goes across the wire.\n joinDescriptor = buildCGJoinMessage(joinDescriptor, 'cancelJob');\n debugging('computeGroups') && console.debug(`cancelJob client: job ${job}, descriptor`, joinDescriptor);\n const { success, payload } = await exports.serviceConnection.send('cancelJob', { job, joinDescriptor });\n\n if (!success) throw new DCPError(`Cannot cancel job ${job} for compute group with ${cgId(joinDescriptor)}:`, payload);\n if (payload && !payload.success) return reconstructServiceError(payload);\n\n return payload;\n};\n\n/**\n * Async function that cancels the owned job in the Compute Group.\n * \n * On the client side the access model in place is that if you know the (user/password)\n * joinKey+joinSecret/joinKey+joinHash/joinKey+joinHashHash/id+joinAddress,\n * you can cancel the jobs in the compute group, where id:=opaqueId from table computeGroups.\n * On the service side the corresponding access model is\n * joinKey+joinHashHash/id+joinAddress .\n * Access is also allowed if the compute group owner is the connection peerAddress.\n * \n * Unless the compute group owner is the connection peerAddress, the descriptor must contain\n * { joinKey, joinHashHash } or { id, joinAddress }\n * where the value of id in { id, joinAddress } is the opaqueId attribute of the row in table computeGroups.\n * \n * @param {cgClientJoinType} joinDescriptor - Array of descriptor objects for the compute groups. This descriptor\n * needs to contain enough information to authorize access to the\n * compute group. Properties may include:\n * - id (id:=opaqueId)\n * - joinKey\n * - joinSecret\n * - joinHash\n * - joinAddress\n * \n * Additional, either the joinKey or id MUST be specified so\n * that we can identify the compute group in question.\n * @returns {Promise<apiClientType>}\n * @access public\n * @example\n * await computeGroup.cancelAllJobs( { joinKey: 'dcpDemo', joinHash: 'eh1-e063976b20a92da97a27b9873465c6f2c9d6e4370befa86c8c1dd312c78befc2' } );\n * await computeGroup.cancelAllJobs( { id: 'bYcYGQ3NOpFnP4FKs6IBQd', joinAddress: 'c15053fc30d4bdf91e2e0bba79578f8b649e55ea' });\n * where the corresponding rows in table computeGroups have attributes\n * row1: joinKey:='dcpDemo', joinHash:='eh1-e063976b20a92da97a27b9873465c6f2c9d6e4370befa86c8c1dd312c78befc2'\n * row2: opaqueId:='bYcYGQ3NOpFnP4FKs6IBQd', joinAddress:='c15053fc30d4bdf91e2e0bba79578f8b649e55ea' .\n */\nexports.cancelAllJobs = async function cancelAllJobs(joinDescriptor)\n{\n if (!exports.serviceConnection)\n await openAndConnectServiceConn();\n\n // Verify joinDescriptor has sufficient information to authorize a compute group (not guarenteed).\n if (!hasSufficientJoinInfo(joinDescriptor))\n return clientError(`cancelAllJobs: Insufficient information to authorize compute group: ${JSON.stringify(joinDescriptor)}.`);\n\n // Translate so that neither joinHash nor joinSecret goes across the wire.\n joinDescriptor = buildCGJoinMessage(joinDescriptor, 'cancelAllJobs');\n debugging('computeGroups') && console.debug('cancelAllJobs client: descriptor', joinDescriptor);\n const { success, payload } = await exports.serviceConnection.send('cancelAllJobs', { joinDescriptor });\n\n if (!success) throw new DCPError(`Cannot cancel owned jobs for compute group with ${cgId(joinDescriptor)}:`, payload);\n if (payload && !payload.success) return reconstructServiceError(payload);\n\n return payload;\n};\n\n/**\n * Calculate a joinHash for a compute group. This is an eh1- hash of the cg salt and \n * joinSecret components of a compute group description.\n *\n * @param {object} details an object containing the cg salt, which is\n * the joinKey if the compute group uses one;\n * otherwise it is the joinAddress. This object\n * may also contain the joinSecret.\n * @param {string} [joinSecret] the join secret -- plain text -- that is\n * the \"password\" for the compute group. If not\n * specified, we use details.joinSecret.\n */\nexports.calculateJoinHash = function computeGroups$calculateJoinHash(details, joinSecret)\n{\n if (typeof joinSecret === 'undefined')\n joinSecret = details.joinSecret;\n\n return hash.calculate(hash.eh1, `${details.joinKey || details.joinAddress} ${joinSecret}`);\n}\n\n\n//# sourceURL=webpack://dcp/./src/dcp-client/compute-groups/index.js?");
4173
+ 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 console.error(`addJobToGroups: Insufficient information to authorize compute group: ${JSON.stringify(joinDescriptor)}.`);\n\n // Translate so that neither joinHash nor joinSecret goes across the wire.\n const message = buildCGJoinMessage(joinDescriptor, 'addJobToGroups');\n debugging('computeGroups') && console.debug(`addJobToGroups client: job ${job}, message`, message);\n\n cgArray.push(message);\n }\n\n const { success, payload } = await exports.serviceConnection.send('addJobToGroups', { job, cgArray });\n\n debugging('computeGroups') && console.debug('addJobToGroups payload', payload);\n\n if (!success) throw new DCPError(`Cannot add job ${job} to compute groups.`, payload);\n if (payload && !payload.success) return reconstructServiceError(payload);\n\n // If the server reported success but did not return a list of CGs (eg. v.4.2.5 server),\n // assume (and inform the client) we added all groups successfully\n return payload || computeGroups;\n};\n\n/**\n * Async function that lists all the Jobs in a Compute Group.\n * \n * The descriptor must have one of the properties joinkey, id (id:=opaqueId).\n * Must be the owner of the Compute Group to list jobs from it.\n * The job does not need to be owned.\n * \n * The descriptor is of the form { joinKey: 'dcpDemo' } or { id: 'bYcYGQ3NOpFnP4FKs6IBQd' }.\n * where 'bYcYGQ3NOpFnP4FKs6IBQd' is the opaqueId of the Compute Group.\n *\n * @param {cgAccessType} descriptor - Must have one of the properties joinKey, id (id:=opaqueId). Specifically\n * descriptor = { joinKey: 'dcpDemo' } or descriptor = { id: opaqueId }\n * @returns {Promise<apiClientType>}\n * @access public\n * @example\n * let listOfJobs1 = await computeGroup.listJobs({ joinKey: 'dcpDemo' });\n * let listOfJobs2 = await computeGroup.listJobs({ id: 'bYcYGQ3NOpFnP4FKs6IBQd' });\n * where the corresponding rows in table computeGroups have attributes\n * row1: joinKey:='dcpDemo'\n * row2: opaqueId:='bYcYGQ3NOpFnP4FKs6IBQd'\n */\nexports.listJobs = async function listJobs(descriptor)\n{\n if (!exports.serviceConnection)\n await openAndConnectServiceConn();\n\n // Verify descriptor has sufficient information to access a compute group (not guarenteed).\n if (!descriptor.joinKey && !descriptor.id)\n return clientError(`listJobs: Insufficient information to identify compute group: ${JSON.stringify(descriptor)}.`);\n\n descriptor = buildCGMessage(descriptor, 'listJobs');\n debugging('computeGroups') && console.debug('listJob client: descriptor', descriptor);\n const { success, payload } = await exports.serviceConnection.send('listJobs', { descriptor });\n\n if (!success) throw new DCPError(`Cannot list jobs for compute group with ${cgId(descriptor)}`, payload);\n if (payload && !payload.success) return reconstructServiceError(payload);\n\n return payload;\n};\n\n/**\n * Async function that removes a job from a Compute Group.\n * \n * The descriptor must have one of the properties joinkey, id (id:=opaqueId).\n * Must be the owner of the Compute Group to remove a job from it.\n * The job does not need to be owned.\n * \n * The descriptor is of the form { joinKey: 'dcpDemo' } or { id: 'bYcYGQ3NOpFnP4FKs6IBQd' }.\n * where 'bYcYGQ3NOpFnP4FKs6IBQd' is the opaqueId of the Compute Group.\n *\n * @param {Address} job - The address of the Job that will be added to the Compute Group.\n * @param {cgAccessType} descriptor - { joinKey: 'dcpDemo' } or { id: 'bYcYGQ3NOpFnP4FKs6IBQd' }\n * @returns {Promise<apiClientType>}\n * @access public\n * @example\n * await computeGroup.removeJob( 'P+Y4IApeFQLrYS2W7MkVg7', { joinKey: 'dcpDemo' });\n * await computeGroup.removeJob( 'P+Y4IApeFQLrYS2W7MkVg7', { id: 'bYcYGQ3NOpFnP4FKs6IBQd' });\n * where the corresponding rows in table computeGroups have attributes\n * row1: joinKey:='dcpDemo'\n * row2: opaqueId:='bYcYGQ3NOpFnP4FKs6IBQd'\n */\nexports.removeJob = async function removeJob(job, descriptor)\n{\n if (!exports.serviceConnection)\n await openAndConnectServiceConn();\n\n // Verify descriptor has sufficient information to access a compute group (not guarenteed).\n if (!descriptor.joinKey && !descriptor.id)\n return clientError(`removeJob: Insufficient information to identify compute group: ${JSON.stringify(descriptor)}.`);\n\n descriptor = buildCGMessage(descriptor, 'removeJob');\n debugging('computeGroups') && console.debug(`removeJob client: job ${job}, descriptor`, descriptor);\n const { success, payload } = await exports.serviceConnection.send('removeJob', { job, descriptor });\n\n if (!success) throw new DCPError(`Cannot remove job ${job} from compute group with ${cgId(descriptor)}`, payload);\n if (payload && !payload.success) return reconstructServiceError(payload);\n\n return payload;\n};\n\n/**\n * Async function that removes all jobs from a Compute Group.\n * \n * The descriptor must have one of the properties joinkey, id (id:=opaqueId).\n * Must be the owner of the Compute Group to remove jobs from it.\n * \n * The descriptor is of the form { joinKey: 'dcpDemo' } or { id: 'bYcYGQ3NOpFnP4FKs6IBQd' }.\n * where 'bYcYGQ3NOpFnP4FKs6IBQd' is the opaqueId of the Compute Group.\n *\n * @param {cgAccessType} descriptor - { joinKey: 'dcpDemo' } or { id: 'bYcYGQ3NOpFnP4FKs6IBQd' }\n * @returns {Promise<apiClientType>}\n * @access public\n * @example\n * await computeGroup.removeAllJobs({ joinKey: 'dcpDemo' });\n * await computeGroup.removeAllJobs({ id: 'bYcYGQ3NOpFnP4FKs6IBQd' });\n * where the corresponding rows in table computeGroups have attributes\n * row1: joinKey:='dcpDemo'\n * row2: opaqueId:='bYcYGQ3NOpFnP4FKs6IBQd'\n */\nexports.removeAllJobs = async function removeAllJobs(descriptor)\n{\n if (!exports.serviceConnection)\n await openAndConnectServiceConn();\n\n // Verify descriptor has sufficient information to access a compute group (not guarenteed).\n if (!descriptor.joinKey && !descriptor.id)\n return clientError(`removeAllJobs: Insufficient information to identify compute group: ${JSON.stringify(descriptor)}.`);\n\n descriptor = buildCGMessage(descriptor, 'removeAllJobs');\n debugging('computeGroups') && console.debug('removeAllJobs client: descriptor', descriptor);\n const { success, payload } = await exports.serviceConnection.send('removeAllJobs', { descriptor });\n\n if (!success) throw new DCPError(`Cannot remove all jobs from compute group with ${cgId(descriptor)}:`, payload);\n if (payload && !payload.success) return reconstructServiceError(payload);\n\n return payload;\n};\n\n/**\n * Async function that cancels the specified owned job.\n * \n * Must be the owner of the job.\n *\n * On the client side the access model in place is that if you know the (user/password)\n * joinKey+joinSecret/joinKey+joinHash/joinKey+joinHashHash/id+joinAddress,\n * you can cancel the job in the compute group, where id:=opaqueId from table computeGroups.\n * On the service side the corresponding access model is\n * joinKey+joinHashHash/id+joinAddress .\n * Access is also allowed if the compute group owner is the connection peerAddress.\n * \n * Unless the compute group owner is the connection peerAddress, the descriptor must contain\n * { joinKey, joinHashHash } or { id, joinAddress }\n * where the value of id in { id, joinAddress } is the opaqueId attribute of the row in table computeGroups.\n * \n * @param {Address} job - The address of the Job that will be added to the Compute Group.\n * @param {cgClientJoinType} joinDescriptor - Array of descriptor objects for the compute groups. This descriptor\n * needs to contain enough information to authorize access to the\n * compute group. Properties may include:\n * - id (id:=opaqueId)\n * - joinKey\n * - joinSecret\n * - joinHash\n * - joinAddress\n *\n * Additional, either the joinKey or id MUST be specified so\n * that we can identify the compute group in question.\n * @returns {Promise<apiClientType>}\n * @access public\n * @example\n * await computeGroup.cancelJob( 'P+Y4IApeFQLrYS2W7MkVg7', { joinKey: 'dcpDemo', joinHash: 'eh1-e063976b20a92da97a27b9873465c6f2c9d6e4370befa86c8c1dd312c78befc2' } );\n * await computeGroup.cancelJob( 'P+Y4IApeFQLrYS2W7MkVg7', { id: 'bYcYGQ3NOpFnP4FKs6IBQd', joinAddress: 'c15053fc30d4bdf91e2e0bba79578f8b649e55ea' });\n * where the corresponding rows in table computeGroups have attributes\n * row1: joinKey:='dcpDemo', joinHash:='eh1-e063976b20a92da97a27b9873465c6f2c9d6e4370befa86c8c1dd312c78befc2'\n * row2: opaqueId:='bYcYGQ3NOpFnP4FKs6IBQd', joinAddress:='c15053fc30d4bdf91e2e0bba79578f8b649e55ea' .\n */\nexports.cancelJob = async function cancelJob(job, joinDescriptor)\n{\n if (!exports.serviceConnection)\n await openAndConnectServiceConn();\n\n // Verify joinDescriptor has sufficient information to authorize a compute group (not guarenteed).\n if (!hasSufficientJoinInfo(joinDescriptor))\n return clientError(`cancelJob: Insufficient information to authorize compute group: ${JSON.stringify(joinDescriptor)}.`);\n\n // Translate so that neither joinHash nor joinSecret goes across the wire.\n joinDescriptor = buildCGJoinMessage(joinDescriptor, 'cancelJob');\n debugging('computeGroups') && console.debug(`cancelJob client: job ${job}, descriptor`, joinDescriptor);\n const { success, payload } = await exports.serviceConnection.send('cancelJob', { job, joinDescriptor });\n\n if (!success) throw new DCPError(`Cannot cancel job ${job} for compute group with ${cgId(joinDescriptor)}:`, payload);\n if (payload && !payload.success) return reconstructServiceError(payload);\n\n return payload;\n};\n\n/**\n * Async function that cancels the owned job in the Compute Group.\n * \n * On the client side the access model in place is that if you know the (user/password)\n * joinKey+joinSecret/joinKey+joinHash/joinKey+joinHashHash/id+joinAddress,\n * you can cancel the jobs in the compute group, where id:=opaqueId from table computeGroups.\n * On the service side the corresponding access model is\n * joinKey+joinHashHash/id+joinAddress .\n * Access is also allowed if the compute group owner is the connection peerAddress.\n * \n * Unless the compute group owner is the connection peerAddress, the descriptor must contain\n * { joinKey, joinHashHash } or { id, joinAddress }\n * where the value of id in { id, joinAddress } is the opaqueId attribute of the row in table computeGroups.\n * \n * @param {cgClientJoinType} joinDescriptor - Array of descriptor objects for the compute groups. This descriptor\n * needs to contain enough information to authorize access to the\n * compute group. Properties may include:\n * - id (id:=opaqueId)\n * - joinKey\n * - joinSecret\n * - joinHash\n * - joinAddress\n * \n * Additional, either the joinKey or id MUST be specified so\n * that we can identify the compute group in question.\n * @returns {Promise<apiClientType>}\n * @access public\n * @example\n * await computeGroup.cancelAllJobs( { joinKey: 'dcpDemo', joinHash: 'eh1-e063976b20a92da97a27b9873465c6f2c9d6e4370befa86c8c1dd312c78befc2' } );\n * await computeGroup.cancelAllJobs( { id: 'bYcYGQ3NOpFnP4FKs6IBQd', joinAddress: 'c15053fc30d4bdf91e2e0bba79578f8b649e55ea' });\n * where the corresponding rows in table computeGroups have attributes\n * row1: joinKey:='dcpDemo', joinHash:='eh1-e063976b20a92da97a27b9873465c6f2c9d6e4370befa86c8c1dd312c78befc2'\n * row2: opaqueId:='bYcYGQ3NOpFnP4FKs6IBQd', joinAddress:='c15053fc30d4bdf91e2e0bba79578f8b649e55ea' .\n */\nexports.cancelAllJobs = async function cancelAllJobs(joinDescriptor)\n{\n if (!exports.serviceConnection)\n await openAndConnectServiceConn();\n\n // Verify joinDescriptor has sufficient information to authorize a compute group (not guarenteed).\n if (!hasSufficientJoinInfo(joinDescriptor))\n return clientError(`cancelAllJobs: Insufficient information to authorize compute group: ${JSON.stringify(joinDescriptor)}.`);\n\n // Translate so that neither joinHash nor joinSecret goes across the wire.\n joinDescriptor = buildCGJoinMessage(joinDescriptor, 'cancelAllJobs');\n debugging('computeGroups') && console.debug('cancelAllJobs client: descriptor', joinDescriptor);\n const { success, payload } = await exports.serviceConnection.send('cancelAllJobs', { joinDescriptor });\n\n if (!success) throw new DCPError(`Cannot cancel owned jobs for compute group with ${cgId(joinDescriptor)}:`, payload);\n if (payload && !payload.success) return reconstructServiceError(payload);\n\n return payload;\n};\n\n/**\n * Calculate a joinHash for a compute group. This is an eh1- hash of the cg salt and \n * joinSecret components of a compute group description.\n *\n * @param {object} details an object containing the cg salt, which is\n * the joinKey if the compute group uses one;\n * otherwise it is the joinAddress. This object\n * may also contain the joinSecret.\n * @param {string} [joinSecret] the join secret -- plain text -- that is\n * the \"password\" for the compute group. If not\n * specified, we use details.joinSecret.\n */\nexports.calculateJoinHash = function computeGroups$calculateJoinHash(details, joinSecret)\n{\n if (typeof joinSecret === 'undefined')\n joinSecret = details.joinSecret;\n\n return hash.calculate(hash.eh1, `${details.joinKey || details.joinAddress} ${joinSecret}`);\n}\n\n\n//# sourceURL=webpack://dcp/./src/dcp-client/compute-groups/index.js?");
4141
4174
 
4142
4175
  /***/ }),
4143
4176
 
@@ -4147,7 +4180,7 @@ eval("/**\n * @file Client facing module that implements Compute Groups API\n
4147
4180
  \***********************************/
4148
4181
  /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
4149
4182
 
4150
- eval("/**\n * @file Module that implements Compute API\n * @module dcp/compute\n * @access public\n */\n\n/* global dcpConfig */\n\n/**\n * @access private - Will change soon!\n * @typedef {object} JobRequirements\n * @property {object} environment\n * @property {boolean} environment.fdlibm - Workers express capability 'fdlibm' in order for client applications to have confidence that results will be bitwise identical across workers. This library is recommended, but not required, for implementaiton of Google chrome, Mozilla Firefox and the DCP standalone worker use fdlibm but Microsoft Edge and Apple Safari do not use it.\n * @property {boolean} environment.offscreenCanvas When present, this capability indicates that the worker environment has the OffscreenCanvas symbol defined.\n * @property {object} details\n * @property {object} details.offscreenCanvas\n * @property {boolean} details.offscreenCanvas.bigTexture4096 This capability indicates that the worker's WebGL MAX_TEXTURE_SIZE is at least 4096.\n * @property {boolean} details.offscreenCanvas.bigTexture8192 This capability indicates that the worker's WebGL MAX_TEXTURE_SIZE is at least 8192.\n * @property {boolean} details.offscreenCanvas.bigTexture16384 This capability indicates that the worker's WebGL MAX_TEXTURE_SIZE is at least 16384.\n * @property {boolean} details.offscreenCanvas.bigTexture32768 This capability indicates that the worker's WebGL MAX_TEXTURE_SIZE is at least 32768.\n * @property {object} engine\n * @property {boolean} engine.es7 This capability enforces that the worker is running an es7-compliant JavaScript engine.\n * @property {boolean} engine.spidermonkey The capability enforces that the worker will run in the SpiderMonkey JS engine.\n * @property {boolean} browser.chrome The capability to exclude Chrome browsers.\n */\n\n/**\n * This JSDoc 'namespace' is a convenient way to document some globals that are available only in\n * the sandbox environment provided to work functions.\n * @access public\n * @namespace sandboxEnv\n */\n\n/**\n * This function emits a progress event. Progress events should be emitted approximately once per second; a task which fails to emit a \n * progress event for a certain period of time will be cancelled by the supervisor. The argument to this function is interpreted to six \n * significant digits, and must increase for every call. *All work functions must emit at least one progress event* - this requirement \n * will be enforced by the estimator. * The period of time mentioned above will be at least 30 wall-clock seconds and at least 30 \n * benchmark-adjusted seconds\n * @access public\n * @method progress\n * @param {string|number|undefined} n a number between 0 and 1 (inclusive) that represents a best-guess \n * at the completed portion of the task as a ratio of completed work to total work. If the argument is a string ending in the `%` symbol, \n * it will be interpreted as a number in the usual mathematical sense. If the argument is `undefined`, the slice progress is noted but \n * the amount of progress is considered to be indeterminate.\n * @memberof module:dcp/compute~sandboxEnv\n * @returns {boolean} true\n */\n\n/**\n * Emit a `console` event to the `JobHandle` in the client. If the sandbox is running in an environment with a native console object, the native method may also be invoked.\n * \n * @member console\n * @property {function} log fire console event with 'log' as `level` (see {@link Job#event:console|Job#event:console.level}).\n * @property {function} debug fire console event with 'debug' as `level` (see {@link Job#event:console|Job#event:console.level}).\n * @property {function} info fire console event with 'info' as `level` (see {@link Job#event:console|Job#event:console.level}).\n * @property {function} warn fire console event with 'warn' as `level` (see {@link Job#event:console|Job#event:console.level}).\n * @property {function} error fire console event with 'error' as `level` (see {@link Job#event:console|Job#event:console.level}).\n * \n * **Note** some ES environments (Chrome, Firefox) implement C-style print formatting in this method. This is currently not supported.\n * @memberof module:dcp/compute~sandboxEnv\n * @emits Job#console\n * @access public\n */\n\n/**\n * @member work An object that contains the following properties\n * @property {function} emit (eventName, ...) - send arbitrary args with `eventName`. This event is received client-side with those args (see {@link Job#work}).\n * @property {object} job\n * @property {object} job.public Property set on job handle, see {@link Job#public}.\n * @memberof module:dcp/compute~sandboxEnv\n * @access public\n */\n\n/**\n * @member {OffscreenCanvas} OffscreenCanvas As defined in the {@link https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvas|HTML Standard}, provides a canvas which can be rendered off-screen. If this interface is not available in a given worker, the worker will not report capability \"offscreenCanvas\".\n * @memberof module:dcp/compute~sandboxEnv\n * @access public\n */\n\nconst protocolV4 = __webpack_require__(/*! dcp/protocol-v4 */ \"./src/protocol-v4/index.js\");\nconst { EventEmitter } = __webpack_require__(/*! dcp/common/dcp-events/event-emitter */ \"./src/common/dcp-events/event-emitter.js\");\nconst { RangeObject, rehydrateRange } = __webpack_require__(/*! dcp/dcp-client/range-object */ \"./src/dcp-client/range-object.js\");\nconst { RemoteDataSet } = __webpack_require__(/*! dcp/dcp-client/remote-data-set */ \"./src/dcp-client/remote-data-set.js\");\nconst { RemoteDataPattern } = __webpack_require__(/*! dcp/dcp-client/remote-data-pattern */ \"./src/dcp-client/remote-data-pattern.js\");\nconst { Job, ResultHandle } = __webpack_require__(/*! dcp/dcp-client/job */ \"./src/dcp-client/job/index.js\");\nconst { jobStatuses } = __webpack_require__(/*! dcp/common/scheduler-constants */ \"./src/common/scheduler-constants.js\");\nconst wallet = __webpack_require__(/*! dcp/dcp-client/wallet */ \"./src/dcp-client/wallet/index.js\");\nconst { DcpURL } = __webpack_require__(/*! dcp/common/dcp-url */ \"./src/common/dcp-url.js\");\nconst { disableWorker } = __webpack_require__(/*! dcp/dcp-client/worker */ \"./src/dcp-client/worker/index.js\");\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\nconst debugging = (__webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope)('dcp-client');\n\nconst debug = (...args) => {\n if (debugging()) {\n args.unshift('dcp-client:compute');\n console.debug(...args);\n }\n};\n\nconst MARKET_VALUE = Symbol('Market Value');\nconst indirectEval = eval; // eslint-disable-line no-eval\n\n/**\n * Instance methods of this class are exported as static members of {@link module:dcp/compute|dcp/compute}.\n * @example\n * let job = compute.for(1, 3, function (i) {\n * progress('100%');\n * return i*10;\n * })\n * let results = await job.exec();\n */\nclass Compute extends EventEmitter {\n constructor () {\n super('Compute');\n\n /** @todo move these into some central config */\n this.defaultMaxGPUs = 1;\n this.GPUsAssigned = 0;\n this.allowSlicesToFinishBeforeStopping = false;\n\n this.schedulerURL = undefined; // Will default to dcpConfig.scheduler.location if not overidden\n this.bankURL = undefined; // Will default to dcpConfig.bank.services.bankTeller.location if not overidden\n\n Object.defineProperty(this.marketValue, MARKET_VALUE, {\n value: true,\n writable: false,\n configurable: false,\n enumerable: false,\n });\n\n const ceci = this;\n this.RemoteDataSet = RemoteDataSet;\n this.RemoteDataPattern = RemoteDataPattern;\n // Initialize to null so these properties are recognized for the Job class\n this.phemeConnection = null;\n this.phemeUses = 0;\n this.deployConnection = null;\n this.openPhemeConn = function openPhemeConn()\n {\n ceci.phemeConnection = new protocolV4.Connection(dcpConfig.scheduler.services.pheme);\n ceci.phemeConnection.on('close', ceci.openPhemeConn);\n }\n\n this.openDeployConn = function openDeployConn()\n {\n ceci.deployConnection = new protocolV4.Connection(dcpConfig.scheduler.services.jobSubmit);\n ceci.deployConnection.on('close', ceci.openDeployConn);\n }\n }\n\n get defaultMaxWorkers () {\n return portalWorker.supervisor.maxWorkingSandboxes\n }\n\n set defaultMaxWorkers (value) {\n portalWorker.supervisor.maxWorkingSandboxes = value\n }\n\n get paymentAddress () {\n return portalWorker.supervisor.paymentAddress\n }\n\n set paymentAddress (value) {\n portalWorker.supervisor.paymentAddress = value\n }\n\n get mining () {\n return portalWorker.working\n }\n\n /** Return a special object that represents the current market value\n * @param {number} factor - OPTIONAL multiplier. default: 1.0\n */\n marketValue (factor = 1) {\n return {\n [MARKET_VALUE]: factor\n };\n }\n\n /** Describes the payment required to compute such a slice on a worker\n * or in a market working at the rates described in the WorkValue object.\n * This function does not take into account job-related overhead.\n * \n * @param {object} sliceProfile A SliceProfile-shaped object\n * @param {object} workValue A WorkValue-shaped object\n * @returns {number} Payment value in DCC describing rate to compute slice\n */\n calculateSlicePayment(sliceProfile, workValue) {\n return sliceProfile.cpuHours * workValue.cpuHour\n + sliceProfile.gpuHours * workValue.gpuHour\n + sliceProfile.inputMBytes * workValue.inputMByte\n + sliceProfile.outputMBytes * workValue.outputMByte;\n }\n\n \n /**\n * Deprecated `mine` API. Use `work` instead\n */\n async mine (numberOfCores, paymentAddress = this.paymentAddress) {\n return this.work(numberOfCores, paymentAddress);\n }\n\n /**\n * Instruct the Worker to stop working, then emit workerInactive\n */\n async stopWorking() {\n console.warn('Using the Compute API for controlling workers is deprecated. Use the Worker API instead.')\n await portalWorker.stop(!this.allowSlicesToFinishBeforeStopping)\n }\n\n /**\n * Deprecated `stopMining` API. Use `stopWorking` instead\n */\n stopMining () {\n console.log(\"Warning: used deprecated 'stopMining' API\");\n this.stopWorking();\n }\n \n /**\n * Flag worker as disabled\n */\n disableWorker() {\n disableWorker();\n }\n\n /**\n * Deprecated `disableMiner` API. Use `disableWorker` instead\n */\n disableMiner() {\n this.disableWorker();\n }\n\n /**\n * Experimental replacement for mine() /* wg aug 2019\n * @param {number} [numberOfCores = this.defaultMaxWorkers] - Number of parallel processes to run\n * @param {string} [paymentAddress = this.paymentAddress || wallet.get().address] - The address of the account to work for\n */\n async work(numCores, paymentAddress) {\n console.warn('Using the Compute API for controlling workers is deprecated. Use the Worker API instead.');\n\n portalWorker.supervisor.paymentAddress = paymentAddress || this.paymentAddress || (await wallet.get()).address;\n portalWorker.supervisor.maxWorkingSandboxes = numCores || this.defaultMaxWorkers || 1;\n await portalWorker.start();\n\n return portalWorker;\n }\n\n /**\n * Form 1. Create a Job for the distributed computer by specifying the start,\n * end, and step for a RangeObject with a given work function and arguments.\n *\n * @param {number} start First number in a range (see {@link RangeObject})\n * @param {number} end Last number in a range (see {@link RangeObject})\n * @param {number} [step=1] the space between numbers in a range (see\n * {@link RangeObject})\n * @param {function | string | URL | DcpURL} work Work function as string or\n * function literal.\n * @param {object} [arguments=[]] optional Array-like object which contains\n * arguments that are passed to the work function\n * @returns {Job}\n * @access public\n * @example\n * let job = compute.for(1, 3, function (i) {\n * progress('100%')\n * return i*10\n * })\n */\n /**\n * Form 2.a Create a Job for the distributed computer by specifying the input\n * as a RemoteDataSet with a given work function and arguments.\n *\n * @param {RemoteDataSet} remoteDataSet Array, ES6 function* generator, or any\n * other type of iterable object.\n * \n * Form 2.b Create a Job for the distributed computer by specifying the input\n * as a RemoteDataPattern with a given work function and arguments.\n * @param {RemoteDataPattern} RemoteDataPattern Array, ES6 function* generator, or any\n * other type of iterable object.\n * @param {function | string | URL | DcpURL} work Work function as string or\n * function literal.\n * @param {object} [arguments=[]] optional Array-like object which contains\n * arguments that are passed to the work function\n * @returns {Job}\n * @access public\n * @example\n * const remoteData = ['https://example.com/data'];\n * const job = compute.for(new RemoteDataSet(...remoteData), function(i) {\n * progress(1)\n * return i\n * })\n * \n * * @example\n * const pattern = 'https://example.com/data/{slice}.json';\n * const NumberOfSlices = 5;\n * const job = compute.for(new RemoteDataSet(pattern, NumberOfSlices), function(i) {\n * progress(1)\n * return i\n * })\n */\n /**\n * Form 3. Create a Job for the distributed computer by specifying the input\n * as an Iterable object with a given work function and arguments.\n *\n * @param {Iterable} iterableObject Array, ES6 function* generator, or any\n * other type of iterable object.\n * @param {function | string | URL | DcpURL} work Work function as string or\n * function literal.\n * @param {object} [arguments=[]] optional Array-like object which contains\n * arguments that are passed to the work function\n * @returns {Job}\n * @access public\n * @example\n * let job = compute.for([123,456], function(i) {\n * progress(1)\n * return i\n * })\n */\n /**\n * Form 4. Create a Job for the distributed computer by specifying the input\n * as a RangeObject with a given work function and arguments.\n *\n * @param {RangeObject} rangeObject Either a RangeObject or an object literal\n * which can be used to create a RangeObject (see {@link RangeObject}).\n * @param {function | string | URL | DcpURL} work Work function as string or\n * function literal.\n * @param {object} [arguments=[]] optional Array-like object which contains\n * arguments that are passed to the work function\n * @returns {Job}\n * @access public\n * @example\n * let job = compute.for({start: 10, end: 13, step: 2}, (i) => progress(1) && i)\n */\n /**\n * Form 5. Similar to Form 4, except with a multi range object containing an\n * array of range objects in the key ranges. These are used to create\n * multi-dimensional ranges, like nested loops. If they were written as\n * traditional loops, the outermost loop would be the leftmost range object,\n * and the innermost loop would be the rightmost range object.\n *\n * @param {MultiRangeObject} multiRange Either a MultiRangeObject or any valid\n * argument to the constructor.\n * @param {function | string | URL | DcpURL} work Work function as string or\n * function literal.\n * @param {object} [arguments=[]] optional Array-like object which contains\n * arguments that are passed to the work function\n * @returns {Job}\n * @access public\n * @example\n * let job = compute.for([{start: 1, end: 2}, {start: 3, end: 5}], function(i,j) {\n * return [i, j, i*j]\n * })\n */\n for(...args) {\n // args, or work function\n const lastArg = args[args.length - 1];\n // work function, if args provided\n const secondLastArg = args[args.length - 2];\n let range = null;\n let work;\n let genArgs;\n\n // All forms: extract work and optional args from the end and validate it.\n if (isValidWorkFunctionType(secondLastArg)) {\n // for(..., workFunction, arguments)\n work = secondLastArg\n if (lastArg && Array.from(lastArg).length > 0 || typeof lastArg === 'string') {\n // array or string, use as-is (string should be a URI)\n genArgs = lastArg;\n } else if (lastArg === undefined) {\n genArgs = [];\n } else {\n throw new TypeError(\n 'Work function arguments (the last parameter), should be an array.',\n );\n }\n } else if (isValidWorkFunctionType(lastArg)) {\n // for(..., workFunction)\n work = lastArg;\n genArgs = [];\n }\n\n if (!isWorkFunctionValid(work)) {\n throw new TypeError(\n `Work parameter must be a function or a string that evaluates to a function, or a URL that points to the location of a function.\\nReceived type ${typeof work} (${work})`,\n );\n }\n\n // Extract the proper information from every form.\n const [firstArg, secondArg, thirdArg] = args;\n if (['object', 'string'].includes(typeof firstArg)) {\n // Forms 2 & 3 (data is iterable, eg. an Array or RemoteDataSet)\n // Forms 4 & 5 (data is RangeObject & Friends)\n range = rehydrateRange(firstArg);\n } else if (typeof firstArg === 'number' && typeof secondArg === 'number') {\n // Form 1 (data is start, end[, step])\n const start = firstArg;\n const end = secondArg;\n\n /**\n * If there is an argument between the end and the work fn, use it as step\n * otherwise set step to undefined.\n */\n const step = typeof thirdArg === 'number' ? thirdArg : undefined;\n range = new RangeObject(start, end, step);\n } else {\n throw new TypeError(\n 'Parameters do not match one of the accepted call forms.',\n );\n }\n\n if (range.length === 0) {\n throw new Error('Length of input set must be greater than zero.');\n }\n\n debug('Work:', work);\n debug('Input Set:', range);\n debug('Arguments:', genArgs);\n return new Job(work, range, genArgs);\n\n /**\n * @param {any} workFunction\n */\n function isValidWorkFunctionType(workFunction) {\n return (\n typeof workFunction === 'function' ||\n typeof workFunction === 'string' ||\n DcpURL.isURL(workFunction)\n );\n }\n\n /**\n * Checks if the workFunction can be evaluated to a function or if it's\n * protocol is HTTPS,\n * @param {function | string | URL | DcpURL} workFunction\n */\n function isWorkFunctionValid(workFunction) {\n switch (typeof workFunction) {\n case 'function':\n return true;\n case 'string': {\n return isValidFunction(workFunction);\n }\n case 'object':\n return DcpURL.isURL(workFunction);\n default:\n return false;\n }\n }\n\n /**\n * Evaluates to true if the string can be coerced into a javascript\n * function.\n * @param {string} workFunction\n */\n function isValidFunction(workFunction) {\n try {\n const fn = indirectEval(`(${workFunction})`);\n return typeof fn === 'function';\n } catch (e) {\n return false;\n }\n }\n }\n\n /**\n * Form 1.\n * Create a job which will run a work function once\n *\n * @param {function | string | URL | DcpURL} work Work function as string or function literal.\n * @param {object} [workFunctionArgs=[]] optional Array-like object which contains arguments which are passed to the work func\n * @returns {Job}\n * @access public\n * @example\n * const job = compute.do(function (i) {\n * progress('100%')\n * return i*10\n * })\n */\n /**\n * Form 2.\n * Create a job which will run a work function on the values 1..n\n *\n * @param {number} n Number of times to run the work function.\n * @param {function | string | URL | DcpURL} work Work function as string or function literal.\n * @param {object} [workFunctionArgs=[]] optional Array-like object which contains arguments which are passed to the work func\n * @returns {Job}\n * @access public\n * @example\n * const job = compute.do(5, function (i) {\n * progress('100%')\n * return i*10\n * })\n */\n do(...args) {\n let n;\n let work;\n let workFunctionArgs;\n if (typeof args[0] !== 'number') {\n // Form 1.\n n = 1;\n [work, workFunctionArgs] = args;\n } else {\n // Form 2.\n [n, work, workFunctionArgs] = args;\n }\n return this.for(0, n - 1, work, workFunctionArgs);\n }\n\n /** Cancel a running job, by opaque id\n *\n * @param {string} id Address of the job to cancel.\n * @param {wallet.Keystore} ownerKeystore The keystore of the job owner\n * @param {string} reason If provided, will be sent on to client\n * @returns {object} The status of the funds that were released from escrow.\n * @throws when id or ownerKeystore are not provided.\n * @access public\n */\n async cancel (id, ownerKeystore, reason = '') {\n if (typeof id !== 'string') {\n throw new Error(`compute.cancel: Job ID must be a string, not ${typeof id}.`);\n }\n\n if (!(ownerKeystore instanceof wallet.Keystore)) {\n throw new Error('compute.cancel: ownerKeystore must be instance of wallet.Keystore');\n }\n\n if (!this.deployConnection)\n this.openDeployConn();\n\n const response = await this.deployConnection.send('cancelJob', {\n job: id,\n owner: ownerKeystore.address,\n reason,\n }, ownerKeystore);\n\n this.deployConnection.off('close', this.deployConnection)\n await this.deployConnection.close();\n\n return response.payload;\n }\n\n /**\n * Reconnect/resume a job, by opaque id\n *\n * @param {string} id Opaque id of the Job to resume/reconnect.\n * @param {wallet.Keystore} [ownerKeystore=wallet.get()] The keystore of the job owner\n * @returns {Job}\n * @throws when id is not provided.\n * @access public\n */\n async resume (id, ownerKeystore) {\n if (typeof id !== 'string') {\n throw new Error(`compute.resume: Job ID must be a string, not ${typeof id}.`);\n }\n\n if (!(ownerKeystore instanceof wallet.Keystore)) {\n ownerKeystore = await wallet.get();\n }\n\n const response = await this.schedulerConnection.send('fetchJob', {\n job: id, // XXX@TODO change 'address' to 'job' once we port this operation to v4\n owner: ownerKeystore.address,\n }, ownerKeystore);\n\n const job = new Job(response.payload);\n job.setPaymentAccountKeystore(ownerKeystore);\n return job;\n }\n\n /**\n * Form 1. Query the status of a job that has been deployed to the scheduler.\n *\n * @access public\n * @param {string|Job} job A job ID or a Job instance\n * @throws when ownerKeystore is not provided\n * @returns {Promise<object>} A status object describing a given job.\n */\n /**\n * Form 2. Query the status of jobs deployed to the scheduler.\n *\n * @access public\n * @param {object} [opts] Query options\n * @param {number} [opts.startTime] Jobs older than this will be excluded from\n * the results.\n * @param {number} [opts.endTime] Jobs newer than this will be excluded from\n * the results.\n * @throws when ownerKeystore is not provided\n * @returns {Promise<Array<Object>>} An array of status objects corresponding to the status of\n * jobs deployed by the given payment account.\n */\n async status(...args) {\n const requestPayload = {};\n\n const firstArg = args.shift();\n if (typeof firstArg === 'string' || firstArg instanceof Job) {\n // Form 1\n requestPayload.jobAddress = firstArg instanceof Job ? firstArg.id : firstArg;\n requestPayload.statuses = jobStatuses;\n } else if (typeof firstArg === 'object') {\n // Form 2\n if (\n (typeof firstArg.startTime !== 'undefined' &&\n typeof firstArg.startTime !== 'number' &&\n !(firstArg.startTime instanceof Date)) ||\n (typeof firstArg.endTime !== 'undefined' &&\n typeof firstArg.endTime !== 'number' &&\n !(firstArg.endTime instanceof Date))\n ) {\n throw new TypeError(\n 'startTime and or endTime were not specified as instances of Date or milliseconds since epoch',\n );\n }\n\n requestPayload.startDeployTime = firstArg.startTime;\n requestPayload.endDeployTime = firstArg.endTime;\n \n if (typeof firstArg.statuses !== 'undefined'\n && (!(firstArg.statuses instanceof Array)\n || !(firstArg.statuses.every((status) => jobStatuses.includes(status))))\n ) {\n throw new TypeError(`Job statuses must be specified in an Array and can only include ${jobStatuses}`)\n }\n \n requestPayload.statuses = firstArg.statuses || jobStatuses;\n \n } else {\n throw new TypeError(`Unknown first argument type ${typeof firstArg}`);\n }\n\n if (!this.deployConnection)\n this.openDeployConn();\n\n const {\n success,\n payload: responsePayload,\n } = await this.deployConnection.send(\n 'listJobs',\n requestPayload\n );\n\n if (!success) {\n throw new DCPError('Request for job status failed', responsePayload);\n }\n\n this.deployConnection.off('close', this.deployConnection)\n await this.deployConnection.close();\n if (responsePayload.length > 0 && (typeof firstArg === 'string' || firstArg instanceof Job)) {\n return responsePayload[0];\n }\n\n return responsePayload;\n }\n\n /**\n * Returns information for the job with the provided ID. It will use\n * `wallet.getId()` to retrieve the auth keystore.\n *\n * @param {string} jobAddress\n * @returns {Promise<object>} status object describing a given job\n * @access public\n */\n async getJobInfo(jobAddress) {\n this.phemeUses++;\n if (!this.phemeConnection)\n this.openPhemeConn();\n\n const ks = await wallet.getId();\n const { payload: jobResponsePayload } = await this.phemeConnection.send('fetchJobReport', \n {\n jobOwner: ks.address,\n job: jobAddress\n });\n\n const { payload: sliceResponsePayload } = await this.phemeConnection.send(\n 'fetchSliceReport', \n {\n jobOwner: ks.address,\n job: jobAddress\n },\n );\n\n const jobReport = {};\n Object.assign(jobReport, jobResponsePayload, sliceResponsePayload);\n\n if (this.phemeConnection && this.phemeUses === 1)\n {\n this.phemeConnection.off('close', this.openPhemeConn);\n this.phemeConnection.close();\n this.phemeConnection = null;\n }\n this.phemeUses--;\n\n return jobReport;\n }\n\n /**\n * Returns information for the slices of the job with the provided ID. It will\n * use `wallet.getId()` to retrieve the auth keystore.\n *\n * @param {string} jobAddress\n * @returns {Promise<object>} A slice status object for the given job.\n * @access public\n */\n async getSliceInfo(jobAddress) {\n this.phemeUses++;\n if (!this.phemeConnection)\n this.openPhemeConn();\n\n const identityKeystore = await wallet.getId();\n const {\n success,\n payload: responsePayload,\n } = await this.phemeConnection.send(\n 'fetchSliceReport',\n {\n job: jobAddress,\n jobOwner: identityKeystore.address,\n },\n identityKeystore,\n );\n\n if (!success) {\n throw new DCPError('Request for slice info failed', responsePayload);\n }\n\n if (this.phemeConnection && this.phemeUses === 1)\n {\n this.phemeConnection.off('close', this.openPhemeConn)\n this.phemeConnection.close();\n this.phemeConnection = null;\n }\n this.phemeUses--;\n\n return responsePayload;\n }\n}\n\nexports.RemoteDataSet = RemoteDataSet;\nexports.RemoteDataPattern = RemoteDataPattern;\nexports.MARKET_VALUE = MARKET_VALUE;\nexports.compute = new Compute(); /* :( */\nexports.compute.getPublicComputeGroupId = () => (__webpack_require__(/*! dcp/common/scheduler-constants */ \"./src/common/scheduler-constants.js\").computeGroups[\"public\"].id);\nexports.compute.getPublicComputeGroupDescriptor = () => (__webpack_require__(/*! dcp/common/scheduler-constants */ \"./src/common/scheduler-constants.js\").computeGroups[\"public\"]);\nexports.compute.ResultHandle = ResultHandle; // export as compute.ResultHandle for instanceof checking\nexports.compute.Job = Job;\nexports.version = {\n api: '1.5.2',\n provides: '1.0.0' /* dcpConfig.scheduler.compatibility.operations.compute */\n};\n\n\n//# sourceURL=webpack://dcp/./src/dcp-client/compute.js?");
4183
+ 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?");
4151
4184
 
4152
4185
  /***/ }),
4153
4186
 
@@ -4157,7 +4190,23 @@ eval("/**\n * @file Module that implements Compute API\n * @module dcp/comput
4157
4190
  \*********************************/
4158
4191
  /***/ ((module, exports, __webpack_require__) => {
4159
4192
 
4160
- eval("/* module decorator */ module = __webpack_require__.nmd(module);\n/**\n * @file dcp-client-bundle-src.js\n * Top-level file which gets webpacked into the bundle consumed by dcp-client 2.5\n * @author Wes Garland, wes@kingsds.network\n * @date July 2019\n */\n\n{\n let thisScript = typeof document !== 'undefined' ? (typeof document.currentScript !== 'undefined' && document.currentScript) || document.getElementById('_dcp_client_bundle') : {}\n let realModuleDeclare\n\n if ( false || typeof module.declare === 'undefined') {\n realModuleDeclare = ( true) ? module.declare : 0\n if (false) {}\n module.declare = function moduleUnWrapper (deps, factory) {\n factory(null, module.exports, module)\n return module.exports\n }\n }\n\n let _debugging = () => false\n dcpConfig.future = (__webpack_require__(/*! ../common/config-future.js */ \"./src/common/config-future.js\").futureFactory)(_debugging, dcpConfig);\n\n /* These modules are official API and must be part of DCP Client */\n let officialApi = {\n 'protocol': __webpack_require__(/*! ../protocol-v4 */ \"./src/protocol-v4/index.js\"),\n 'compute': (__webpack_require__(/*! ./compute */ \"./src/dcp-client/compute.js\").compute),\n 'worker': __webpack_require__(/*! ./worker */ \"./src/dcp-client/worker/index.js\"),\n 'wallet': __webpack_require__(/*! ./wallet */ \"./src/dcp-client/wallet/index.js\"),\n };\n\n /* Allow client programs to use modules which happen to be in the bundle anyhow */\n let conveniencePeers = {\n 'ethereumjs-wallet': (__webpack_require__(/*! ./wallet/keystore */ \"./src/dcp-client/wallet/keystore.js\")._internalEth.wallet),\n 'ethereumjs-util': (__webpack_require__(/*! ./wallet/keystore */ \"./src/dcp-client/wallet/keystore.js\")._internalEth.util),\n 'socket.io-client': __webpack_require__(/*! socket.io-client */ \"./node_modules/socket.io-client/build/cjs/index.js\"),\n 'bignumber.js': __webpack_require__(/*! bignumber.js */ \"./node_modules/bignumber.js/bignumber.js\"),\n 'semver': __webpack_require__(/*! semver */ \"./node_modules/semver/semver.js\"),\n };\n\n /* Some of these modules are API-track. Some of them need to be published to be\n * available for top-level resolution by DCP internals. Those (mostly) should have\n * been written using relative module paths.....\n */\n let modules = Object.assign({\n 'dcp-build': {\"version\":\"b5864d51c1e7a9b3024698306574b60003a91d62\",\"branch\":\"release\",\"dcpClient\":{\"version\":\"4.2.9\",\"from\":\"git+ssh://git@gitlab.com/Distributed-Compute-Protocol/dcp-client.git#prod-20220718\",\"resolved\":\"git+ssh://git@gitlab.com/Distributed-Compute-Protocol/dcp-client.git#15ef46ae27df39b0d1383bd282d36f8c3227905e\"},\"built\":\"Fri Jul 22 2022 09:34:47 GMT-0400 (Eastern Daylight Saving Time)\",\"config\":{\"generated\":\"Fri 22 Jul 2022 09:34:45 AM EDT by erose on lorge\",\"build\":\"debug\"},\"webpack\":\"5.70.0\",\"node\":\"v14.20.0\"},\n 'dcp-xhr': __webpack_require__(/*! ../common/dcp-xhr */ \"./src/common/dcp-xhr.js\"),\n 'dcp-env': __webpack_require__(/*! ../common/dcp-env */ \"./src/common/dcp-env.js\"),\n 'dcp-url': __webpack_require__(/*! ../common/dcp-url */ \"./src/common/dcp-url.js\"),\n 'cli': __webpack_require__(/*! ../common/cli */ \"./src/common/cli.js\"),\n 'dcp-timers': __webpack_require__(/*! ../common/dcp-timers */ \"./src/common/dcp-timers.js\"),\n 'dcp-dot-dir': __webpack_require__(/*! ../common/dcp-dot-dir */ \"./src/common/dcp-dot-dir.js\"),\n 'dcp-assert': __webpack_require__(/*! ../common/dcp-assert */ \"./src/common/dcp-assert.js\"),\n 'dcp-events': __webpack_require__(/*! ../common/dcp-events */ \"./src/common/dcp-events/index.js\"),\n 'utils': __webpack_require__(/*! ../utils */ \"./src/utils/index.js\"),\n 'debugging': __webpack_require__(/*! ../debugging */ \"./src/debugging.js\"),\n 'publish': __webpack_require__(/*! ../common/dcp-publish */ \"./src/common/dcp-publish.js\"),\n 'compute-groups': {\n ...__webpack_require__(/*! ./compute-groups */ \"./src/dcp-client/compute-groups/index.js\"),\n publicGroupOpaqueId: (__webpack_require__(/*! ../common/scheduler-constants */ \"./src/common/scheduler-constants.js\").computeGroups[\"public\"].opaqueId),\n },\n 'bank-util': __webpack_require__(/*! ./bank-util */ \"./src/dcp-client/bank-util.js\"),\n 'protocol-v4': __webpack_require__(/*! ../protocol-v4 */ \"./src/protocol-v4/index.js\"), /* deprecated */\n 'client-modal': __webpack_require__(/*! ./client-modal */ \"./src/dcp-client/client-modal/index.js\"),\n 'legacy-modal': (__webpack_require__(/*! ../../portal/www/js/modal */ \"./portal/www/js/modal.js\").Modal),\n 'eth': __webpack_require__(/*! ./wallet/eth */ \"./src/dcp-client/wallet/eth.js\"),\n 'serialize': __webpack_require__(/*! ../utils/serialize */ \"./src/utils/serialize.js\"),\n 'job': __webpack_require__(/*! ./job */ \"./src/dcp-client/job/index.js\"),\n 'range-object': __webpack_require__(/*! ./range-object */ \"./src/dcp-client/range-object.js\"),\n 'stats-ranges': __webpack_require__(/*! ./stats-ranges */ \"./src/dcp-client/stats-ranges.js\"),\n 'standard-objects': {}\n }, conveniencePeers, officialApi);\n\n /* Export the JS Standard Classes (etc) from the global object of the bundle evaluation context,\n * in case we have code somewhere that needs to use these for instanceof checks.\n */\n ;[ Object, Function, Boolean, Symbol,\n Error, EvalError, RangeError, ReferenceError, SyntaxError, TypeError, URIError,\n Number, Math, Date,\n String, RegExp,\n Array, Int8Array, Uint8Array, Uint8ClampedArray, Int16Array, Uint16Array, Int32Array, Uint32Array, Float32Array, Float64Array,\n Map, Set, WeakMap, WeakSet,\n ArrayBuffer, DataView, JSON,\n Promise, \n Reflect, Proxy, Intl, WebAssembly, __webpack_require__\n ].forEach(function (obj) {\n if (obj.name && (typeof obj === 'function' || typeof obj === 'object'))\n modules['standard-objects'][obj.name] = obj\n })\n\n if (typeof BigInt !== 'undefined')\n modules['standard-objects']['BigInt'] === BigInt;\n if (typeof BigInt64Array !== 'undefined')\n modules['standard-objects']['BigInt64Array'] === BigInt64Array;\n if (typeof BigInt64Array !== 'undefined')\n modules['standard-objects']['BigUint64Array'] === BigUint64Array;\n\n module.declare([], function(require, exports, module) {\n Object.assign(exports, modules)\n exports['dcp-config'] = dcpConfig\n })\n if (realModuleDeclare)\n module.declare = realModuleDeclare\n\n bundleExports = thisScript.exports = exports; /* must be last expression evaluated! */\n}\n\n\n//# sourceURL=webpack://dcp/./src/dcp-client/index.js?");
4193
+ <<<<<<< HEAD
4194
+ 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\":\"4dd484422936278679ed70ce7501d11964b9d792\",\"branch\":\"release\",\"dcpClient\":{\"version\":\"4.2.11\",\"from\":\"git+ssh://git@gitlab.com/Distributed-Compute-Protocol/dcp-client.git#prod-20220815\",\"resolved\":\"git+ssh://git@gitlab.com/Distributed-Compute-Protocol/dcp-client.git#abc5acb71dc4f13fc06a196f98e3c58f01bed960\"},\"built\":\"Thu Aug 18 2022 09:29:50 GMT-0400 (Eastern Daylight Saving Time)\",\"config\":{\"generated\":\"Thu 18 Aug 2022 09:29:47 AM EDT by erose on lorge\",\"build\":\"debug\"},\"webpack\":\"5.70.0\",\"node\":\"v14.20.0\"},\n 'dcp-xhr': __webpack_require__(/*! ../common/dcp-xhr */ \"./src/common/dcp-xhr.js\"),\n 'dcp-env': __webpack_require__(/*! ../common/dcp-env */ \"./src/common/dcp-env.js\"),\n 'dcp-url': __webpack_require__(/*! ../common/dcp-url */ \"./src/common/dcp-url.js\"),\n 'cli': __webpack_require__(/*! ../common/cli */ \"./src/common/cli.js\"),\n 'dcp-timers': __webpack_require__(/*! ../common/dcp-timers */ \"./src/common/dcp-timers.js\"),\n 'dcp-dot-dir': __webpack_require__(/*! ../common/dcp-dot-dir */ \"./src/common/dcp-dot-dir.js\"),\n 'dcp-assert': __webpack_require__(/*! ../common/dcp-assert */ \"./src/common/dcp-assert.js\"),\n 'dcp-events': __webpack_require__(/*! ../common/dcp-events */ \"./src/common/dcp-events/index.js\"),\n 'utils': __webpack_require__(/*! ../utils */ \"./src/utils/index.js\"),\n 'debugging': __webpack_require__(/*! ../debugging */ \"./src/debugging.js\"),\n 'publish': __webpack_require__(/*! ../common/dcp-publish */ \"./src/common/dcp-publish.js\"),\n 'compute-groups': {\n ...__webpack_require__(/*! ./compute-groups */ \"./src/dcp-client/compute-groups/index.js\"),\n publicGroupOpaqueId: (__webpack_require__(/*! ../common/scheduler-constants */ \"./src/common/scheduler-constants.js\").computeGroups[\"public\"].opaqueId),\n },\n 'bank-util': __webpack_require__(/*! ./bank-util */ \"./src/dcp-client/bank-util.js\"),\n 'protocol-v4': __webpack_require__(/*! ../protocol-v4 */ \"./src/protocol-v4/index.js\"), /* deprecated */\n 'client-modal': __webpack_require__(/*! ./client-modal */ \"./src/dcp-client/client-modal/index.js\"),\n 'legacy-modal': (__webpack_require__(/*! ../../portal/www/js/modal */ \"./portal/www/js/modal.js\").Modal),\n 'eth': __webpack_require__(/*! ./wallet/eth */ \"./src/dcp-client/wallet/eth.js\"),\n 'serialize': __webpack_require__(/*! ../utils/serialize */ \"./src/utils/serialize.js\"),\n 'job': __webpack_require__(/*! ./job */ \"./src/dcp-client/job/index.js\"),\n 'range-object': __webpack_require__(/*! ./range-object */ \"./src/dcp-client/range-object.js\"),\n 'stats-ranges': __webpack_require__(/*! ./stats-ranges */ \"./src/dcp-client/stats-ranges.js\"),\n 'standard-objects': {}\n }, conveniencePeers, officialApi);\n\n /* Export the JS Standard Classes (etc) from the global object of the bundle evaluation context,\n * in case we have code somewhere that needs to use these for instanceof checks.\n */\n ;[ Object, Function, Boolean, Symbol,\n Error, EvalError, RangeError, ReferenceError, SyntaxError, TypeError, URIError,\n Number, Math, Date,\n String, RegExp,\n Array, Int8Array, Uint8Array, Uint8ClampedArray, Int16Array, Uint16Array, Int32Array, Uint32Array, Float32Array, Float64Array,\n Map, Set, WeakMap, WeakSet,\n ArrayBuffer, DataView, JSON,\n Promise, \n Reflect, Proxy, Intl, WebAssembly, __webpack_require__\n ].forEach(function (obj) {\n if (obj.name && (typeof obj === 'function' || typeof obj === 'object'))\n modules['standard-objects'][obj.name] = obj\n })\n\n if (typeof BigInt !== 'undefined')\n modules['standard-objects']['BigInt'] === BigInt;\n if (typeof BigInt64Array !== 'undefined')\n modules['standard-objects']['BigInt64Array'] === BigInt64Array;\n if (typeof BigInt64Array !== 'undefined')\n modules['standard-objects']['BigUint64Array'] === BigUint64Array;\n\n module.declare([], function(require, exports, module) {\n Object.assign(exports, modules)\n exports['dcp-config'] = dcpConfig\n })\n if (realModuleDeclare)\n module.declare = realModuleDeclare\n\n bundleExports = thisScript.exports = exports; /* must be last expression evaluated! */\n}\n\n\n//# sourceURL=webpack://dcp/./src/dcp-client/index.js?");
4195
+ ||||||| e0e85dd
4196
+ 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\":\"057ccd920bfd464401288eed9b98f438ace69b2e\",\"branch\":\"prod-20220815\",\"dcpClient\":{\"version\":\"4.2.11\",\"from\":\"git+ssh://git@gitlab.com/Distributed-Compute-Protocol/dcp-client.git#prod-20220815\",\"resolved\":\"git+ssh://git@gitlab.com/Distributed-Compute-Protocol/dcp-client.git#abc5acb71dc4f13fc06a196f98e3c58f01bed960\"},\"built\":\"Thu Aug 18 2022 09:25:47 GMT-0400 (Eastern Daylight Saving Time)\",\"config\":{\"generated\":\"Thu 18 Aug 2022 09:25:45 AM EDT by erose on lorge\",\"build\":\"debug\"},\"webpack\":\"5.70.0\",\"node\":\"v14.20.0\"},\n 'dcp-xhr': __webpack_require__(/*! ../common/dcp-xhr */ \"./src/common/dcp-xhr.js\"),\n 'dcp-env': __webpack_require__(/*! ../common/dcp-env */ \"./src/common/dcp-env.js\"),\n 'dcp-url': __webpack_require__(/*! ../common/dcp-url */ \"./src/common/dcp-url.js\"),\n 'cli': __webpack_require__(/*! ../common/cli */ \"./src/common/cli.js\"),\n 'dcp-timers': __webpack_require__(/*! ../common/dcp-timers */ \"./src/common/dcp-timers.js\"),\n 'dcp-dot-dir': __webpack_require__(/*! ../common/dcp-dot-dir */ \"./src/common/dcp-dot-dir.js\"),\n 'dcp-assert': __webpack_require__(/*! ../common/dcp-assert */ \"./src/common/dcp-assert.js\"),\n 'dcp-events': __webpack_require__(/*! ../common/dcp-events */ \"./src/common/dcp-events/index.js\"),\n 'utils': __webpack_require__(/*! ../utils */ \"./src/utils/index.js\"),\n 'debugging': __webpack_require__(/*! ../debugging */ \"./src/debugging.js\"),\n 'publish': __webpack_require__(/*! ../common/dcp-publish */ \"./src/common/dcp-publish.js\"),\n 'compute-groups': {\n ...__webpack_require__(/*! ./compute-groups */ \"./src/dcp-client/compute-groups/index.js\"),\n publicGroupOpaqueId: (__webpack_require__(/*! ../common/scheduler-constants */ \"./src/common/scheduler-constants.js\").computeGroups[\"public\"].opaqueId),\n },\n 'bank-util': __webpack_require__(/*! ./bank-util */ \"./src/dcp-client/bank-util.js\"),\n 'protocol-v4': __webpack_require__(/*! ../protocol-v4 */ \"./src/protocol-v4/index.js\"), /* deprecated */\n 'client-modal': __webpack_require__(/*! ./client-modal */ \"./src/dcp-client/client-modal/index.js\"),\n 'legacy-modal': (__webpack_require__(/*! ../../portal/www/js/modal */ \"./portal/www/js/modal.js\").Modal),\n 'eth': __webpack_require__(/*! ./wallet/eth */ \"./src/dcp-client/wallet/eth.js\"),\n 'serialize': __webpack_require__(/*! ../utils/serialize */ \"./src/utils/serialize.js\"),\n 'job': __webpack_require__(/*! ./job */ \"./src/dcp-client/job/index.js\"),\n 'range-object': __webpack_require__(/*! ./range-object */ \"./src/dcp-client/range-object.js\"),\n 'stats-ranges': __webpack_require__(/*! ./stats-ranges */ \"./src/dcp-client/stats-ranges.js\"),\n 'standard-objects': {}\n }, conveniencePeers, officialApi);\n\n /* Export the JS Standard Classes (etc) from the global object of the bundle evaluation context,\n * in case we have code somewhere that needs to use these for instanceof checks.\n */\n ;[ Object, Function, Boolean, Symbol,\n Error, EvalError, RangeError, ReferenceError, SyntaxError, TypeError, URIError,\n Number, Math, Date,\n String, RegExp,\n Array, Int8Array, Uint8Array, Uint8ClampedArray, Int16Array, Uint16Array, Int32Array, Uint32Array, Float32Array, Float64Array,\n Map, Set, WeakMap, WeakSet,\n ArrayBuffer, DataView, JSON,\n Promise, \n Reflect, Proxy, Intl, WebAssembly, __webpack_require__\n ].forEach(function (obj) {\n if (obj.name && (typeof obj === 'function' || typeof obj === 'object'))\n modules['standard-objects'][obj.name] = obj\n })\n\n if (typeof BigInt !== 'undefined')\n modules['standard-objects']['BigInt'] === BigInt;\n if (typeof BigInt64Array !== 'undefined')\n modules['standard-objects']['BigInt64Array'] === BigInt64Array;\n if (typeof BigInt64Array !== 'undefined')\n modules['standard-objects']['BigUint64Array'] === BigUint64Array;\n\n module.declare([], function(require, exports, module) {\n Object.assign(exports, modules)\n exports['dcp-config'] = dcpConfig\n })\n if (realModuleDeclare)\n module.declare = realModuleDeclare\n\n bundleExports = thisScript.exports = exports; /* must be last expression evaluated! */\n}\n\n\n//# sourceURL=webpack://dcp/./src/dcp-client/index.js?");
4197
+ =======
4198
+ 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\":\"d4f755b478f851a75b532e258d99ea2c45d288fb\",\"branch\":\"prod-20220907\",\"dcpClient\":{\"version\":\"git+ssh://git@gitlab.com/Distributed-Compute-Protocol/dcp-client.git#ba271b8faf17b3120d92311587ea382e5e716fa0\",\"from\":\"git+ssh://git@gitlab.com/Distributed-Compute-Protocol/dcp-client.git#prod-20220907\",\"resolved\":\"git+ssh://git@gitlab.com/Distributed-Compute-Protocol/dcp-client.git#ba271b8faf17b3120d92311587ea382e5e716fa0\"},\"built\":\"Thu Sep 08 2022 16:23:20 GMT-0400 (Eastern Daylight Saving Time)\",\"config\":{\"generated\":\"Thu 08 Sep 2022 04:23:18 PM EDT by erose on lorge\",\"build\":\"debug\"},\"webpack\":\"5.70.0\",\"node\":\"v14.20.0\"},\n 'dcp-xhr': __webpack_require__(/*! ../common/dcp-xhr */ \"./src/common/dcp-xhr.js\"),\n 'dcp-env': __webpack_require__(/*! ../common/dcp-env */ \"./src/common/dcp-env.js\"),\n 'dcp-url': __webpack_require__(/*! ../common/dcp-url */ \"./src/common/dcp-url.js\"),\n 'cli': __webpack_require__(/*! ../common/cli */ \"./src/common/cli.js\"),\n 'dcp-timers': __webpack_require__(/*! ../common/dcp-timers */ \"./src/common/dcp-timers.js\"),\n 'dcp-dot-dir': __webpack_require__(/*! ../common/dcp-dot-dir */ \"./src/common/dcp-dot-dir.js\"),\n 'dcp-assert': __webpack_require__(/*! ../common/dcp-assert */ \"./src/common/dcp-assert.js\"),\n 'dcp-events': __webpack_require__(/*! ../common/dcp-events */ \"./src/common/dcp-events/index.js\"),\n 'utils': __webpack_require__(/*! ../utils */ \"./src/utils/index.js\"),\n 'debugging': __webpack_require__(/*! ../debugging */ \"./src/debugging.js\"),\n 'publish': __webpack_require__(/*! ../common/dcp-publish */ \"./src/common/dcp-publish.js\"),\n 'compute-groups': {\n ...__webpack_require__(/*! ./compute-groups */ \"./src/dcp-client/compute-groups/index.js\"),\n publicGroupOpaqueId: (__webpack_require__(/*! ../common/scheduler-constants */ \"./src/common/scheduler-constants.js\").computeGroups[\"public\"].opaqueId),\n },\n 'bank-util': __webpack_require__(/*! ./bank-util */ \"./src/dcp-client/bank-util.js\"),\n 'protocol-v4': __webpack_require__(/*! ../protocol-v4 */ \"./src/protocol-v4/index.js\"), /* deprecated */\n 'client-modal': __webpack_require__(/*! ./client-modal */ \"./src/dcp-client/client-modal/index.js\"),\n 'legacy-modal': (__webpack_require__(/*! ../../portal/www/js/modal */ \"./portal/www/js/modal.js\").Modal),\n 'eth': __webpack_require__(/*! ./wallet/eth */ \"./src/dcp-client/wallet/eth.js\"),\n 'serialize': __webpack_require__(/*! ../utils/serialize */ \"./src/utils/serialize.js\"),\n 'job': __webpack_require__(/*! ./job */ \"./src/dcp-client/job/index.js\"),\n 'range-object': __webpack_require__(/*! ./range-object */ \"./src/dcp-client/range-object.js\"),\n 'stats-ranges': __webpack_require__(/*! ./stats-ranges */ \"./src/dcp-client/stats-ranges.js\"),\n '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?");
4199
+
4200
+ /***/ }),
4201
+
4202
+ /***/ "./src/dcp-client/job-values.js":
4203
+ /*!**************************************!*\
4204
+ !*** ./src/dcp-client/job-values.js ***!
4205
+ \**************************************/
4206
+ /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
4207
+
4208
+ 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 which instanciates an instance of RemoteDatum,\n * which is also an instance of 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?");
4209
+ >>>>>>> origin/prod-20220907
4161
4210
 
4162
4211
  /***/ }),
4163
4212
 
@@ -4168,7 +4217,7 @@ eval("/* module decorator */ module = __webpack_require__.nmd(module);\n/**\n *
4168
4217
  /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
4169
4218
 
4170
4219
  "use strict";
4171
- eval("/**\n * @file job/index.js\n * @author Eddie Roosenmaallen, eddie@kingsds.network\n * Matthew Palma, mpalma@kingsds.network\n * Wes Garland, wes@kingsds.network\n * Paul, paul@kingsds.network\n * Ryan Saweczko, ryansaweczko@kingsds.network\n * @date November 2018\n * November 2018\n * February 2022\n * May 2022\n * Jun 2022\n *\n * This module implements the Compute API's Job Handle\n *\n */\n/** @typedef {import('dcp/dcp-client/wallet/keystore').Keystore} Keystore */\n\n\nconst { BigNumber } = __webpack_require__(/*! bignumber.js */ \"./node_modules/bignumber.js/bignumber.js\");\nconst { v4: uuidv4 } = __webpack_require__(/*! uuid */ \"./node_modules/uuid/dist/esm-browser/index.js\");\nconst { EventEmitter, PropagatingEventEmitter } = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\");\nconst { RangeObject, MultiRangeObject, DistributionRange, SuperRangeObject, SparseRangeObject } = __webpack_require__(/*! dcp/dcp-client/range-object */ \"./src/dcp-client/range-object.js\");\nconst { fetchURI, encodeDataURI, dumpObject } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\nconst { getTextEncoder, createTempFile } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\nconst serialize = __webpack_require__(/*! dcp/utils/serialize */ \"./src/utils/serialize.js\");\nconst wallet = __webpack_require__(/*! dcp/dcp-client/wallet */ \"./src/dcp-client/wallet/index.js\");\nconst protocolV4 = __webpack_require__(/*! dcp/protocol-v4 */ \"./src/protocol-v4/index.js\");\nconst { EventSubscriber } = __webpack_require__(/*! dcp/events/event-subscriber */ \"./src/events/event-subscriber.js\");\nconst { DcpURL } = __webpack_require__(/*! dcp/common/dcp-url */ \"./src/common/dcp-url.js\");\nconst ClientModal = __webpack_require__(/*! dcp/dcp-client/client-modal */ \"./src/dcp-client/client-modal/index.js\");\nconst { Worker } = __webpack_require__(/*! dcp/dcp-client/worker */ \"./src/dcp-client/worker/index.js\");\nconst { RemoteDataSet } = __webpack_require__(/*! dcp/dcp-client/remote-data-set */ \"./src/dcp-client/remote-data-set.js\");\nconst { RemoteDataPattern } = __webpack_require__(/*! dcp/dcp-client/remote-data-pattern */ \"./src/dcp-client/remote-data-pattern.js\");\nconst { ResultHandle } = __webpack_require__(/*! ./result-handle */ \"./src/dcp-client/job/result-handle.js\");\nconst { SlicePaymentOffer } = __webpack_require__(/*! ./slice-payment-offer */ \"./src/dcp-client/job/slice-payment-offer.js\");\nconst { addSlices } = __webpack_require__(/*! ./upload-slices */ \"./src/dcp-client/job/upload-slices.js\");\nconst DCP_ENV = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\nconst dcpPublish = __webpack_require__(/*! dcp/common/dcp-publish */ \"./src/common/dcp-publish.js\");\nconst computeGroups = __webpack_require__(/*! dcp/dcp-client/compute-groups */ \"./src/dcp-client/compute-groups/index.js\");\nconst schedulerConstants = __webpack_require__(/*! dcp/common/scheduler-constants */ \"./src/common/scheduler-constants.js\");\nconst { sliceStatus } = __webpack_require__(/*! dcp/common/scheduler-constants */ \"./src/common/scheduler-constants.js\");\nconst { jobStatus } = __webpack_require__(/*! dcp/common/scheduler-constants */ \"./src/common/scheduler-constants.js\");\nconst bankUtil = __webpack_require__(/*! dcp/dcp-client/bank-util */ \"./src/dcp-client/bank-util.js\");\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\nconst debugging = (__webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope)('dcp-client');\nconst kvin = __webpack_require__(/*! kvin */ \"./node_modules/kvin/kvin.js\");\nlet tunedKvin;\n\nconst log = (...args) => {\n if (debugging('job')) {\n console.debug('dcp-client:job', ...args);\n }\n};\n\nconst ON_BROWSER = DCP_ENV.isBrowserPlatform;\nconst sideloaderModuleIdentifier = 'sideloader-v1';\n\n\nconst DEFAULT_REQUIREMENTS = {\n engine: {\n es7: null,\n spidermonkey: null\n },\n environment: {\n webgpu: null,\n offscreenCanvas: null,\n fdlibm: null\n },\n browser: {\n chrome: null\n },\n details: {\n offscreenCanvas: {\n bigTexture4096: null,\n bigTexture8192: null,\n bigTexture16384: null,\n bigTexture32768: null,\n }\n },\n discrete: null,\n useStrict: null,\n};\n\n/** @typedef {import('../range-object').RangeLike} RangeLike */\n\n/**\n * Ensure input data is an appropriate format\n * @param {RangeObject | DistributionRange | RemoteDataSet | Array | Iterable}\n * inputData - A URI-shaped string, a [Multi]RangeObject-constructing value, or\n * an array of slice data\n * @return {RangeObject | RangeLike | DistributionRange | RemoteDataSet | Array}\n * The coerced input in an appropriate format ([Multi]RangeObject,\n * DistributionRange, RemoteDataSet, or array)\n */\n const wrangleData = (inputData) => {\n\n if (RangeObject.isRangelike(inputData)) { return inputData }\n if (RangeObject.isRangeObject(inputData)) { return inputData }\n if (DistributionRange.isDistribution(inputData)) { return inputData }\n if (inputData instanceof SparseRangeObject) { return inputData }\n if (inputData instanceof MultiRangeObject) { return inputData }\n if (MultiRangeObject.isProtoMultiRangelike(inputData)) { return new MultiRangeObject(inputData) }\n if (RangeObject.isProtoRangelike(inputData)) { return new RangeObject(inputData) }\n if (DistributionRange.isProtoDistribution(inputData)) { return new DistributionRange(inputData) }\n if (RemoteDataSet.isRemoteDataSet(inputData)) { return inputData }\n if (RemoteDataPattern.isRemoteDataPattern(inputData)) { return inputData }\n\n return Array.isArray(inputData) ? inputData : [inputData];\n};\n\n/**\n * @classdesc The Compute API's Job Handle (see {@link https://docs.dcp.dev/specs/compute-api.html#job-handles|Compute API spec})\n * Job handles are objects which correspond to jobs. \n * They are created by some exports of the compute module, such as {@link module:dcp/compute.do|compute.do} and {@link module:dcp/compute.for|compute.for}.\n * @extends module:dcp/dcp-events.PropagatingEventEmitter\n * @hideconstructor\n * @access public\n */\nclass Job extends PropagatingEventEmitter\n{\n /**\n * Fired when the job is accepted by the scheduler on deploy.\n * \n * @event Job#accepted\n * @access public\n * @type {object}\n *//**\n * Fired when the job is cancelled.\n * \n * @event Job#cancel\n * @access public\n *//**\n * Fired when a result is returned.\n * \n * @event Job#result\n * @access public\n * @type {object}\n * @property {string} jobAddress Address of the job\n * @property {string} task ID of the task (slice) the result came from\n * @property {number} sort The index of the slice\n * @property {object} result\n * @property {string} result.request\n * @property {*} result.result The value returned from the work function\n *//**\n * Fired when the result handle is modified, either when a new `result` event is fired or when the results are populated with `results.fetch()`\n * \n * @event Job#resultsUpdated\n * @access public\n *//**\n * Fired when the job has been completed.\n * \n * @event Job#complete\n * @access public\n * @type {ResultHandle}\n *//**\n * Fired when the job's status changes.\n * \n * @event Job#status\n * @access public\n * @type {object}\n * @property {string} jobAddress Address of the job\n * @property {number} total Total number of slices in the job\n * @property {number} distributed Number of slices that have been distributed\n * @property {number} computed Number of slices that have completed execution (returned a result)\n * @property {string} runStatus Current runStatus of the job\n *//**\n * Fired when a slice throws an error.\n * \n * @event Job#error\n * @access public\n * @type {object}\n * @property {string} jobAddress Address of the job\n * @property {number} sliceIndex Index of the slice that threw the error\n * @property {string} message The error message\n * @property {string} stack The error stacktrace\n * @property {string} name The error type name\n *//**\n * Fired when a slice uses one of the console log functions.\n * \n * @event Job#console\n * @access public\n * @type {object}\n * @property {string} jobAddress Address of the job\n * @property {number} sliceIndex The index of the slice that produced this event\n * @property {string} level The log level, one of `debug`, `info`, `log`, `warn`, or `error`\n * @property {string} message The console log message\n *//**\n * Fired when a slice is stopped for not calling progress. Contains information about how long the slice ran for, and about the last reported progress calls.\n * \n * @event Job#noProgress\n * @access public\n * @type {object}\n * @property {string} jobAddress Address of the job\n * @property {number} sliceIndex The index of the slice that failed due to no progress\n * @property {number} timestamp How long the slice ran before failing\n * @property {object} progressReports\n * @property {object} progressReports.last The last progress report received from the worker\n * @property {number} progressReports.last.timestamp Time since the start of the slice\n * @property {number} progressReports.last.progress Progress value reported\n * @property {*} progressReports.last.value The last value that was passed to the progress function\n * @property {number} progressReports.last.throttledReports Number of calls to progress that were throttled since the last report\n * @property {object} progressReports.lastUpdate The last determinate (update to the progress param) progress report received from the worker\n * @property {number} progressReports.lastUpdate.timestamp\n * @property {number} progressReports.lastUpdate.progress\n * @property {*} progressReports.lastUpdate.value\n * @property {number} progressReports.lastUpdate.throttledReports\n *//**\n @todo: is this in the spec? is there a no progress data? should there be?\n * Identical to `noProgress`, except that it also contains the data that the slice was executed with.\n * \n * @event Job#noProgressData\n * @access public\n * @type {object}\n * @property {*} data The data that the slice was executed with\n *//**\n * Fired when the job is paused due to running out of funds. The job can be resumed by escrowing more funds then resuming the job.\n * @todo: is this a thing, should it be a thing (the payload)\n * Event payload is the estimated funds required to complete the job\n * \n * @event Job#nofunds\n * @access public\n * @type {BigNumber}\n *//**\n * Fired when the job cannot be deployed due to no bank account / not enough balance to deploy the job\n * \n * @event Job#ENOFUNDS\n * @access public\n *//**\n * Fired when the job is cancelled due to the work function not calling the `progress` method frequently enough.\n * \n * @event Job#ENOPROGRESS\n * @access public\n *//**\n * The job was cancelled because scheduler has determined that individual tasks in this job exceed the maximum allowable execution time.\n * \n * @event Job#ESLICETOOSLOW\n * @access public\n *//**\n * Fired when the job is cancelled because too many work functions are terminating with uncaught exceptions.\n * \n * @event Job#ETOOMANYERRORS\n * @access public\n */\n\n /**\n * @form1 new Job('application_worker_address'[, data[, arguments]])\n * @form2a new Job('worker source'[, data[, arguments]])\n * @form2b new Job(worker_function[, data[, arguments]])\n */\n constructor ()\n {\n super('Job');\n if (typeof arguments[0] === 'function')\n arguments[0] = arguments[0].toString();\n\n if (typeof arguments[0] === 'string')\n {\n const { encodeDataURI } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\n this.workFunctionURI = encodeDataURI(arguments[0], 'application/javascript');\n } \n else if (DcpURL.isURL(arguments[0]))\n this.workFunctionURI = arguments[0].href;\n\n this.jobInputData = wrangleData(arguments[1] || []);\n this.jobArguments = wrangleData(arguments[2] || []);\n \n log('wrangledInputData:', this.jobInputData);\n log('wrangledArguments:', this.jobArguments);\n\n this.initEventSystems();\n\n /**\n * An object describing the cost the user believes each the average slice will incur, in terms of CPU/GPU and I/O.\n * If defined, this object is used to provide initial scheduling hints and to calculate escrow amounts.\n *\n * @type {object}\n * @access public\n */\n this.initialSliceProfile = undefined;\n \n // The max value that the client is willing to spend to deploy\n // (list on the scheduler, doesn't include compute payment)\n // maxDeployPayment is the max the user is willing to pay to DCP (as a\n // Hold), in addition to the per-slice offer and associated scrape.\n // Currently calculated as `deployCost = costPerKB *\n // (JSON.stringify(job).length / 1024) // 1e-9 per kb`\n // @todo: figure this out / er nov 2018\n this.maxDeployPayment = 1;\n\n /**\n * An object describing the requirements that workers must have to be eligible for this job. See\n * {@link https://docs.dcp.dev/specs/compute-api.html#requirements-objects|Requirements Objects}.\n *\n * @type {object}\n * @access public\n */\n this.requirements = JSON.parse(JSON.stringify(DEFAULT_REQUIREMENTS));\n\n /**\n * A place to store public-facing attributes of the job. Anything stored on this object will be available inside the work \n * function (see {@link module:dcp/compute~sandboxEnv.work}). The properties documented here may be used by workers to display what jobs are currently being \n * worked on.\n * @access public\n * @property {string} name Public-facing name of this job.\n * @property {string} description Public-facing description for this job.\n * @property {string} link Public-facing link to external resource about this job.\n */\n this.public = {\n name: null,\n description: null,\n link: null,\n };\n\n /**\n * A number (can be null, undefined, or infinity) describing the estimationSlicesRemaining in the jpd (dcp-2593)\n * @type {number}\n * @access public\n */\n this.estimationSlices = undefined;\n /**\n * When true, allows a job in estimation to have requestTask return multiple estimation slices.\n * This flag applies independent of infinite estimation, viz., this.estimationSlices === null .\n * @type {boolean}\n * @access public\n */\n this.greedyEstimation = false;\n /**\n * tunable parameters per job\n * @access public\n * @param {object} tuning \n * @param {string} tuning.kvin Encode the TypedArray into a string, trying multiple methods to determine optimum \n * size/performance. The this.tune variable affects the behavior of this code this:\n * @param {boolean} speed If true, only do naive encoding: floats get represented as byte-per-digit strings\n * @param {boolean} size If true, try the naive, ab8, and ab16 encodings; pick the smallest\n * If both are false try the naive encoding if under typedArrayPackThreshold and use if smaller\n * than ab8; otherwise, use ab8\n */\n this.tuning = {\n kvin: {\n size: false,\n speed: false,\n },\n }\n /* For API interface to end-users only */\n Object.defineProperty(this, 'id', {\n get: () => this.address,\n set: (id) => { this.address = id }\n });\n \n this.uuid = uuidv4(); /** @see {@link https://kingsds.atlassian.net/browse/DCP-1475?atlOrigin=eyJpIjoiNzg3NmEzOWE0OWI4NGZkNmI5NjU0MWNmZGY2OTYzZDUiLCJwIjoiaiJ9|Jira Issue} */\n this.dependencies = []; /* dependencies of the work function */\n this.requirePath = []; /* require path for dependencies */\n this.connected = false; /* true when exec or resume called */\n this.results = new ResultHandle(this); /* result handle */\n this.collateResults = true; /* option to receive results as they are computed & ensure all are received on finish */\n this.force100pctCPUDensity = false; /* tell scheduler to assume this job uses 100% cpu density */\n this.workerConsole = false; /* tell workers to log more information about failures in the evaluator this job causes */\n this.address = null; /* job address, created by scheduler during exec call. */\n this.paymentAccountKeystore = null; /* keystore for payment for job to come from */\n this.status = { /* job status details */\n runStatus: null,\n total: null,\n distributed: null,\n computed: null\n };\n // Compute groups. Add to public compute group by default\n this.computeGroups = [ Object.assign({}, schedulerConstants.computeGroups.public) ];\n \n // Update the ready state as we go through job deployment\n this.readyState = sliceStatus.new;\n const that = this;\n this.readyStateChange = function job$$readyStateChange (readyState)\n {\n that.readyState = readyState;\n that.emit('readyStateChange', that.readyState);\n }\n }\n \n /**\n * Initialize the various event systems the job handle requires. These include:\n * - an internal event emitter (this.ee)\n * - an event emitter for any events emitted on `work.emit` within work functions (this.work)\n * - an event subscriber to subscribe (to receive) events from the scheduler (this.eventSubscriber)\n */\n initEventSystems ()\n {\n // Handle the various event-related things required in the constructor\n\n // Internal event emitter for events within job handle\n this.ee = new EventEmitter('Job Internal');\n\n /**\n * An EventEmitter for custom events dispatched by the work function.\n * @type {module:dcp/dcp-events.EventEmitter}\n * @access public\n * @example\n * // in work function\n * work.emit('myEventName', 1, [2], \"three\");\n * // client-side\n * job.work.on('myEventName', (num, arr, string) => { });\n */\n this.work = new EventEmitter('job.work');\n this.listenForCustomEvents = false;\n\n // Event subscriber - receive events from the event router\n this.eventSubscriber = new EventSubscriber(this);\n \n // Some events from the event subscriber can't be emitted immediately upon receipt without having \n // weird/wrong output due to things like serialization. We allow interceptors in the event subscriber\n // to handle this.\n const that = this\n var lastConsoleEv;\n var sameCounter = 1;\n const parseConsole = function deserializeConsoleMessage(ev) {\n if (tunedKvin)\n ev.message = tunedKvin.unmarshal(ev.message);\n else \n ev.message = kvin.unmarshal(ev.message);\n \n if (lastConsoleEv && ev.message[0] === lastConsoleEv.message[0] && ev.sliceNumber === lastConsoleEv.sliceNumber && ev.level === lastConsoleEv.level)\n ev.same = ++sameCounter;\n else\n sameCounter = 1;\n lastConsoleEv = ev;\n \n /* if we have the same message being logged (same sliceNumber, message, log level), the console event object will have the sole property same, nothing else */\n if (ev.same > 1)\n that.emit('console', { same: ev.same });\n else\n {\n delete ev.same;\n that.emit('console', ev);\n }\n }\n\n this.eventIntercepts = {\n result: (ev) => this.handleResult(ev),\n status: (ev) => this.handleStatus(ev),\n cancel: (ev) => this.ee.emit('stopped', ev),\n custom: (ev) => this.work.emit(ev.customEvent, ev),\n console: parseConsole,\n };\n \n this.eventTypes = (__webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\").eventTypes);\n\n this.work.on('newListener', (evt) => {\n this.listenForCustomEvents = true;\n });\n this.desiredEvents = []\n this.on('newListener', (evt) => {\n if (!this.connected && evt !== 'newListener')\n this.desiredEvents.push(evt);\n if (evt === 'cancel')\n this.listeningForCancel = true;\n });\n }\n \n /** \n * Cancel the job\n * @access public\n * @param {string} reason If provided, will be sent to client\n */\n async cancel (reason = undefined)\n {\n const response = await this.useDeployConnection('cancelJob', {\n job: this.address,\n owner: this.paymentAccountKeystore.address,\n reason,\n }, this.paymentAccountKeystore);\n\n return response.payload;\n }\n\n /** \n * Resume this job\n * @access public\n */\n async resume ()\n {\n const response = await this.schedulerConnection.send('resumeJob', {\n job: this.address,\n owner: this.paymentAccountKeystore.address,\n }, this.paymentAccountKeystore);\n\n return response.payload;\n }\n\n /**\n * Helper function for retrieving info about the job. The job must have already been deployed.\n * An alias for {@link module:dcp/compute.getJobInfo}.\n * @access public\n */\n getJobInfo ()\n {\n return (__webpack_require__(/*! ../compute */ \"./src/dcp-client/compute.js\").compute.getJobInfo)(this.address);\n }\n \n /**\n * Helper function for retrieving info about the job's slices. The job must have already been deployed.\n * An alias for {@link module:dcp/compute.getSliceInfo}.\n * @access public\n */\n getSliceInfo ()\n {\n return (__webpack_require__(/*! ../compute */ \"./src/dcp-client/compute.js\").compute.getSliceInfo)(this.address);\n }\n \n /** Escrow additional funds for this job\n * @access public\n * @param {number|BigNumber} fundsRequired - A number or BigNumber instance representing the funds to escrow for this job\n */\n async escrow (fundsRequired)\n {\n if ((typeof fundsRequired !== 'number' && !BigNumber.isBigNumber(fundsRequired))\n || fundsRequired <= 0 || !Number.isFinite(fundsRequired) || Number.isNaN(fundsRequired))\n throw new Error(`Job.escrow: fundsRequired must be a number greater than zero. (not ${fundsRequired})`);\n\n const bankConnection = new protocolV4.Connection(dcpConfig.bank.services.bankTeller);\n\n /*\n * escrow has been broken for an unknown amount of time. `feeStructureId` is not defined anywhere in the job class, and hasn't\n * for a period of time. When fixed, `this[INTERNAL_SYMBOL].payloadDetails.feeStructureId` will likely become just `this.feeStructureId`,\n * but it's being left alone until someone spends the time to fix escrow. / rs Jul 2022\n */\n const response = await bankConnection.send('embiggenFeeStructure', {\n feeStructureAddress: this[INTERNAL_SYMBOL].payloadDetails.feeStructureId,\n additionalEscrow: new BigNumber(fundsRequired),\n fromAddress: this.paymentAccountKeystore.address,\n }, this.paymentAccountKeystore);\n\n bankConnection.close();\n const receipt = response.payload;\n return receipt;\n }\n\n /**\n * create bundles for local dependencies\n */\n _pack ()\n {\n var retval = (__webpack_require__(/*! ./node-modules */ \"./src/dcp-client/job/node-modules.js\").createModuleBundle)(this.dependencies);\n return retval;\n }\n\n /** \n * Collect all of the dependencies together, throw them into a BravoJS\n * module which sideloads them as a side effect of declaration, and transmit\n * them to the package manager. Then we return the package descriptor object,\n * which is guaranteed to have only one file in it.\n *\n * @returns {object} with properties name and files[0]\n */\n async _publishLocalModules ()\n {\n const { tempFile, hash, unresolved } = await this._pack();\n\n if (!tempFile) {\n return { unresolved };\n }\n\n const sideloaderFilename = tempFile.filename;\n const pkg = {\n name: `dcp-pkg-v1-localhost-${hash.toString('hex')}`,\n version: '1.0.0',\n files: {\n [sideloaderFilename]: `${sideloaderModuleIdentifier}.js`,\n },\n }\n\n await dcpPublish.publish(pkg);\n tempFile.remove();\n\n return { pkg, unresolved };\n }\n \n /**\n * This function specifies a module dependency (when the argument is a string)\n * or a list of dependencies (when the argument is an array) of the work\n * function. This function can be invoked multiple times before deployment.\n * @param {string | string[]} modulePaths - A string or array describing one\n * or more dependencies of the job.\n * @access public\n */\n requires (modulePaths)\n {\n if (typeof modulePaths !== 'string' && (!Array.isArray(modulePaths) || modulePaths.some((modulePath) => typeof modulePath !== 'string')))\n throw new TypeError('The argument to dependencies is not a string or an array of strings');\n else if (modulePaths.length === 0)\n throw new RangeError('The argument to dependencies cannot be an empty string or array');\n else if (Array.isArray(modulePaths) && modulePaths.some((modulePath) => modulePath.length === 0))\n throw new RangeError('The argument to dependencies cannot be an array containing an empty string');\n\n if (!Array.isArray(modulePaths))\n modulePaths = [modulePaths];\n\n for (const modulePath of modulePaths)\n {\n if (modulePath[0] !== '.' && modulePath.indexOf('/') !== -1)\n {\n const modulePrefixRegEx = /^(.*)\\/.*?$/;\n const [, modulePrefix] = modulePath.match(modulePrefixRegEx);\n if (modulePrefix && this.requirePath.indexOf(modulePrefix) === -1)\n this.requirePath.push(modulePrefix);\n }\n this.dependencies.push(modulePath);\n }\n }\n \n /** Set the account upon which funds will be drawn to pay for the job.\n * @param {module:dcp/wallet.AuthKeystore} keystore A keystore that representa a bank account.\n * @access public\n */\n setPaymentAccountKeystore (keystore)\n {\n if (this.address)\n {\n if (!keystore.address.eq(this.paymentAccountKeystore))\n {\n let message = 'Cannot change payment account after job has been deployed';\n this.emit('EPERM', message);\n throw new Error(`EPERM: ${message}`);\n }\n }\n \n if (!(keystore instanceof wallet.Keystore))\n throw new Error('Not an instance of Keystore: ' + keystore.toString());\n this.paymentAccountKeystore = keystore;\n }\n \n /** Set the slice payment offer. This is equivalent to the first argument to exec.\n * @param {number} slicePaymentOffer - The number of DCC the user is willing to pay to compute one slice of this job\n */\n setSlicePaymentOffer (slicePaymentOffer)\n {\n this.slicePaymentOffer = new SlicePaymentOffer(slicePaymentOffer);\n }\n \n \n /**\n * @param {URL|DcpURL} locationUrl - A URL object\n * @param {object} postParams - An object with any parameters that a user would like to be passed to a \n * remote result location. This object is capable of carry API keys for S3, \n * DropBox, etc. These parameters are passed as parameters in an \n * application/x-www-form-urlencoded request.\n */\n setResultStorage (locationUrl, postParams)\n {\n if (locationUrl instanceof URL || locationUrl instanceof DcpURL)\n this.resultStorageDetails = locationUrl;\n else\n throw new Error('Not an instance of a DCP URL: ' + locationUrl);\n \n\n // resultStorageParams contains any post params required for off-prem storage\n if (typeof postParams !== 'undefined' && typeof postParams === 'object' )\n this.resultStorageParams = postParams;\n else\n throw new Error('Not an instance of a object: ' + postParams);\n\n // Some type of object here\n this.resultStorageType = 'pattern';\n }\n \n /**\n * This function is identical to exec, except that the job is executed locally\n * in the client.\n * @async\n * @param {number} cores - the number of local cores in which to execute the job.\n * @param {...any} args - The remaining arguments are identical to the arguments of exec\n * @return {Promise<ResultHandle>} - resolves with the results of the job, rejects on an error\n * @access public\n */\n localExec (cores = 1, ...args)\n {\n this.inLocalExec = true;\n this.estimationSlices = 0;\n this.greedyEstimation = false;\n this.isCI = false;\n\n let worker;\n this.on('accepted', () => {\n // Start a worker for this job\n worker = new Worker({\n localExec: true,\n jobAddresses: [this.address],\n allowedOrigins: this.localExecAllowedFiles,\n paymentAddress: this.paymentAccountKeystore.address,\n maxWorkingSandboxes: cores,\n sandboxOptions: {\n ignoreNoProgress: true,\n SandboxConstructor: (DCP_ENV.platform === 'nodejs'\n && (__webpack_require__(/*! ../worker/evaluators */ \"./src/dcp-client/worker/evaluators/index.js\").nodeEvaluatorFactory)())\n },\n });\n\n worker.start().catch((e) => {\n console.error('Failed to start worker for localExec:');\n console.error(e.message);\n });\n });\n\n return this.exec(...args).finally(() => {\n if (worker) {\n setTimeout(() => {\n // stop the worker\n worker.stop(true);\n }, 3000);\n }\n });\n }\n\n /**\n * Deploys the job to the scheduler.\n * @param {number | object} [slicePaymentOffer=compute.marketValue] - Amount\n * in DCC that the user is willing to pay per slice.\n * @param {Keystore} [paymentAccountKeystore=wallet.get] - An instance of the\n * Wallet API Keystore that's used as the payment account when executing the\n * job.\n * @param {object} [initialSliceProfile] - An object describing the cost the\n * user believes the average slice will incur.\n * @access public\n * @emits Job#accepted\n */\n async exec (slicePaymentOffer = (__webpack_require__(/*! ../compute */ \"./src/dcp-client/compute.js\").compute.marketValue), paymentAccountKeystore, initialSliceProfile)\n {\n if (this.connected)\n throw new Error('Exec called twice on the same job handle.');\n \n if (this.estimationSlices === Infinity)\n this.estimationSlices = null;\n else if (this.estimationSlices < 0)\n throw new Error('Incorrect value for estimationSlices; it can be an integer or Infinity!');\n \n if (this.tuning.kvin.speed || this.tuning.kvin.size)\n {\n tunedKvin = new kvin.KVIN();\n tunedKvin.tune = 'size';\n if(this.tuning.kvin.speed)\n tunedKvin.tune = 'speed';\n // If both size and speed are true, kvin will optimize based on speed\n if(this.tuning.kvin.speed && this.tuning.kvin.size)\n console.log('Slices and arguments are being uploaded with speed optimization.');\n }\n \n this.readyStateChange('exec');\n if ((typeof slicePaymentOffer === 'number') || (typeof slicePaymentOffer === 'object')\n || ((this.slicePaymentOffer === null || this.slicePaymentOffer === undefined) && typeof slicePaymentOffer === 'function'))\n this.setSlicePaymentOffer(slicePaymentOffer);\n if (typeof initialSliceProfile !== 'undefined')\n this.initialSliceProfile = initialSliceProfile;\n \n if (typeof paymentAccountKeystore !== 'undefined')\n {\n /** XXX @todo deprecate use of ethereum wallet objects */\n if (typeof paymentAccountKeystore === 'object' && paymentAccountKeystore.hasOwnProperty('_privKey'))\n {\n console.warn('* deprecated API * - job.exec invoked with ethereum wallet object as paymentAccountKeystore') /* /wg oct 2019 */\n paymentAccountKeystore = paymentAccountKeystore._privKey\n }\n /** XXX @todo deprecate use of private keys */\n if (wallet.isPrivateKey(paymentAccountKeystore))\n {\n console.warn('* deprecated API * - job.exec invoked with private key as paymentAccountKeystore') /* /wg dec 2019 */\n paymentAccountKeystore = await new wallet.Keystore(paymentAccountKeystore, '');\n }\n\n this.setPaymentAccountKeystore(paymentAccountKeystore)\n }\n \n if (this.paymentAccountKeystore)\n // Throws if they fail to unlock, we allow this since the keystore was set programmatically. \n await this.paymentAccountKeystore.unlock(undefined, parseFloat(dcpConfig.job.maxDeployTime));\n else\n {\n // If not set programmatically, we keep trying to get an unlocked keystore ... forever.\n let locked = true;\n let safety = 0; // no while loop shall go unguarded\n let ks;\n do\n {\n ks = null;\n // custom message for the browser modal to denote the purpose of keystore submission\n let msg = `This application is requesting a keystore file to execute ${this.public.description || this.public.name || 'this job'}. Please upload the corresponding keystore file. If you upload a keystore file which has been encrypted with a passphrase, the application will not be able to use it until it prompts for a passphrase and you enter it.`;\n try\n {\n ks = await wallet.get({ contextId: this.contextId, jobName: this.public.name, msg});\n }\n catch (e)\n {\n if (e.code !== ClientModal.CancelErrorCode) throw e;\n };\n if (ks)\n {\n try\n {\n await ks.unlock(undefined, parseFloat(dcpConfig.job.maxDeployTime));\n locked = false;\n }\n catch (e)\n {\n // prompt user again if user enters password incorrectly, exit modal otherwise\n if (e.code !== wallet.unlockFailErrorCode) throw e;\n }\n }\n if (safety++ > 1000) throw new Error('EINFINITY: job.exec tried wallet.get more than 1000 times.')\n } while (locked);\n this.setPaymentAccountKeystore(ks)\n }\n \n // We either have a valid keystore + password or we have rejected by this point.\n if (!this.slicePaymentOffer)\n throw new Error('A payment profile must be assigned before executing the job');\n else\n this.feeStructure = this.slicePaymentOffer.toFeeStructure(this.jobInputData.length);\n\n if (!this.address)\n {\n try\n {\n this.readyStateChange('init');\n await this.deployJob();\n const listenersPromise = this.addInitialEvents();\n const computeGroupsPromise = this.joinComputeGroups();\n let uploadSlicePromise;\n // if job data is by value then upload data to the scheduler in a staggered fashion\n if (Array.isArray(this.dataValues))\n {\n this.readyStateChange('uploading');\n uploadSlicePromise = addSlices(this.dataValues, this.address, tunedKvin).then(() => this.close());\n debugging('slice-upload') && uploadSlicePromise.then(() => console.debug('831: x-dbg-uploadComplete'));\n }\n \n // await all promises for operations that can be done after the job is deployed\n await Promise.all([listenersPromise, computeGroupsPromise, uploadSlicePromise]);\n\n this.readyStateChange('deployed');\n this.emit('accepted', { job: this });\n }\n catch (error)\n {\n if (ON_BROWSER)\n await ClientModal.alert(error, { title: 'Failed to deploy job!' });\n throw error;\n }\n }\n else\n {\n // reconnecting to an old job\n await this.addInitialEvents();\n this.readyStateChange('reconnected');\n }\n\n this.connected = true;\n\n return new Promise((resolve, reject) => {\n const onComplete = () => resolve(this.results);\n const onCancel = (event) => {\n /**\n * FIXME(DCP-1150): Remove this since normal cancel event is noisy\n * enough to not need stopped event too.\n */\n if (ON_BROWSER && !this.listeningForCancel)\n ClientModal.alert('More details in console...', { title: 'Job Canceled' });\n this.emit('cancel', event);\n\n let errorMsg = event.reason;\n if (event.error)\n errorMsg = errorMsg +`\\n Recent error message: ${event.error.message}`\n \n reject(new DCPError(errorMsg, event.code));\n };\n\n this.ee.once('stopped', async (stopEvent) => {\n // There is a chance the result submitter will emit finished > 1 time. Only handle it once.\n if (this.receivedStop)\n return;\n this.receivedStop = true;\n this.emit('stopped', stopEvent.runStatus);\n switch (stopEvent.runStatus) {\n case jobStatus.finished:\n if (this.collateResults)\n {\n let report = await this.getJobInfo();\n let allSliceNumbers = Array.from(Array(report.totalSlices)).map((e,i)=>i+1);\n let remainSliceNumbers = allSliceNumbers.filter((e) => !this.results.isAvailable(e));\n\n if (remainSliceNumbers.length)\n {\n const promises = remainSliceNumbers.map(sliceNumber => this.results.fetch([sliceNumber], true));\n await Promise.all(promises);\n }\n }\n\n this.emit('complete', this.results);\n onComplete();\n break;\n case jobStatus.cancelled:\n onCancel(stopEvent);\n break;\n default:\n /**\n * Asserting that we should never be able to reach here. The only\n * scheduler events that should trigger the Job's 'stopped' event\n * are jobStatus.cancelled, jobStatus.finished, and sliceStatus.paused.\n */\n reject(new Error(`Unknown event \"${stopEvent.runStatus}\" caused the job to be stopped.`));\n break;\n }\n });\n\n }).finally(() => {\n const handleErr = (e) => {\n console.error('Error while closing job connection:');\n console.error(e);\n }\n\n // Create an async IIFE to not block the promise chain\n (async () => {\n // delay to let last few events to be received\n await new Promise((resolve) => setTimeout(resolve, 1000));\n \n // close all of the connections so that we don't cause node processes to hang.\n this.closeDeployConnection();\n await this.eventSubscriber.close().catch(handleErr);\n await computeGroups.closeServiceConnection().catch((err) => {\n console.error('Warning: could not close compute groups service connection', err);\n });\n })();\n });\n }\n \n /**\n * job.addListeners(): Private function used to set up event listeners to the scheduler\n * before deploying the job.\n */\n async addInitialEvents ()\n {\n this.readyStateChange('listeners');\n\n // This is important: We need to flush the task queue before adding listeners\n // because we queue pending listeners by listening to the newListener event (in the constructor).\n // If we don't flush here, then the newListener events may fire after this function has run,\n // and the events won't be properly set up.\n await new Promise(resolve => setTimeout(resolve, 0));\n\n // @todo: Listen for an estimated cost, probably emit an \"estimated\" event when it comes in?\n // also @todo: Do the estimation task(s) on the scheduler and send an \"estimated\" event\n\n // Always listen to the stop event. It will resolve the work function promise, so is always needed.\n this.on('stop', (ev) => {this.ee.emit('stopped', ev)});\n\n // Connect listeners that were set up before exec\n if (this.desiredEvents.includes('result'))\n this.listeningForResults = true;\n await this.subscribeNewEvents(this.desiredEvents);\n\n // Connect listeners that are set up after exec\n this.on('newListener', (evt) => {\n if (evt === 'newListener' || this.desiredEvents.includes(evt))\n return;\n this.subscribeNewEvents([evt]);\n });\n \n // automatically add a listener for results if collateResults is on\n if (this.collateResults && !this.listeningForResults)\n this.on('result', () => {});\n\n debugging('dcp-client') && console.debug('subscribedEvents', this.desiredEvents);\n\n // If we have listeners for job.work, subscribe to custom events\n if (this.listenForCustomEvents)\n await this.subscribeCustomEvents();\n // Connect work event listeners that are set up after exec\n else\n this.work.on('newListener', () => this.subscribeCustomEvents());\n }\n \n /**\n * Subscribes to either reliable events or optional events. It is assumed that\n * any call to this function will include only new events.\n * @param {string[]} events \n */\n async subscribeNewEvents (events)\n {\n const reliableEvents = [];\n const optionalEvents = [];\n for (let eventName of events)\n {\n eventName = eventName.toLowerCase();\n if (this.eventTypes[eventName] && this.eventTypes[eventName].reliable)\n reliableEvents.push(eventName);\n else if (this.eventTypes[eventName] && !this.eventTypes[eventName].reliable)\n optionalEvents.push(eventName);\n else\n debugging('dcp-client') && console.debug(`Job handler has listener ${eventName} which isn't an event-router event.`);\n }\n if (debugging('dcp-client'))\n {\n console.debug('reliableEvents', reliableEvents);\n console.debug('optionalEvents', optionalEvents);\n }\n await this.eventSubscriber.subscribeManyEvents(reliableEvents, optionalEvents, { filter: { job: this.address } });\n }\n \n /**\n * Establishes listeners for worker events when requested by the client\n */\n async subscribeCustomEvents ()\n {\n if (!this.listeningForCustomEvents)\n await this.eventSubscriber.subscribeManyEvents([], ['custom'], { filter: { job: this.address } });\n this.listeningForCustomEvents = true\n }\n \n async joinComputeGroups ()\n {\n // localExec jobs are not entered in any compute group.\n if (!this.inLocalExec && this.computeGroups && this.computeGroups.length)\n {\n this.readyStateChange('compute-groups');\n computeGroups.addRef(); // Just in case we're doing a Promise.all on multiple execs.\n\n // Add this job to its currently-defined compute groups (as well as public group, if included)\n let success;\n try\n {\n const cgPayload = await computeGroups.addJobToGroups(this.address, this.computeGroups);\n success = true; // To support older version of CG service where addJobToGroups had void/undefined return.\n if (cgPayload) success = cgPayload.success;\n debugging('dcp-client') && console.debug('job/index: addJobToGroups cgPayload:', cgPayload ? cgPayload : 'cgPayload is not defined; probably from legacy CG service.');\n }\n catch (e)\n {\n debugging('dcp-client') && console.debug('job/index: addJobToGroups threw exception:', e);\n success = false;\n }\n\n computeGroups.closeServiceConnection().catch((err) => {\n console.error('Warning: could not close compute groups service connection', err)\n });\n\n /* Could not put the job in any compute group, even though the user wanted it to run. Cancel the job. */\n if (!success)\n {\n await this.cancel('compute-groups::Unable to join any compute groups');\n throw new DCPError(`Access Denied::Failed to add job ${this.address} to any of the desired compute groups`, 'DCPL-1100');\n }\n }\n }\n \n /**\n * Takes result events as input, stores the result and fires off\n * events on the job handle as required. (result, duplicate-result)\n *\n * @param {object} ev - the event recieved from protocol.listen('/results/0xThisGenAdr')\n */\n async handleResult (ev)\n {\n if (this.results === null)\n // This should never happen - the onResult event should only be established/called\n // in addListeners which should also initialize the internal results array\n throw new Error('Job.onResult was invoked before initializing internal results');\n \n const { result: _result, time } = ev.result;\n debugging('dcp-client') && console.debug('handleResult', _result);\n let result = await fetchURI(_result);\n\n if (this.results.isAvailable(ev.sliceNumber))\n {\n const changed = JSON.stringify(this.results[ev.sliceNumber]) !== JSON.stringify(result);\n this.emit('duplicate-result', { sliceNumber: ev.sliceNumber, changed });\n }\n\n this.results.newResult(result, ev.sliceNumber);\n this.emit('result', { job: this.address, sliceNumber: ev.sliceNumber, result });\n this.emit('resultsUpdated');\n }\n \n /**\n * Receives status events from the scheduler, updates the local status object\n * and emits a 'status' event\n *\n * @param {object} ev - the status event received from\n * protocol.listen('/status/0xThisGenAdr')\n * @param {boolean} emitStatus - value indicating whether or not the status\n * event should be emitted\n */\n handleStatus ({ runStatus, total, distributed, computed }, emitStatus = true)\n {\n Object.assign(this.status, {\n runStatus,\n total,\n distributed,\n computed,\n });\n\n if (emitStatus)\n this.emit('status', { ...this.status, job: this.address });\n }\n \n /**\n * Sends a request to the scheduler to deploy the job.\n */\n async deployJob ()\n {\n var moduleDependencies; \n \n /* Send sideloader bundle to the package server */\n if (DCP_ENV.platform === 'nodejs' && this.dependencies.length)\n {\n try\n {\n let { pkg, unresolved } = await this._publishLocalModules();\n\n moduleDependencies = unresolved;\n if (pkg)\n moduleDependencies.push(pkg.name + '/' + sideloaderModuleIdentifier); \n }\n catch(error)\n {\n throw new DCPError(`Error trying to communicate with package manager server: ${error}`);\n }\n }\n else\n moduleDependencies = this.dependencies;\n \n this.readyStateChange('preauth');\n\n const adhocId = this.uuid.slice(this.uuid.length - 6, this.uuid.length);\n const schedId = await dcpConfig.scheduler.identity;\n // The following check is needed for when using dcp-rtlink and loading the config through source, instead of using the dcp-client bundle\n let schedIdAddress = schedId;\n if(schedId.address)\n schedIdAddress = schedId.address;\n const myId = await wallet.getId();\n const preauthToken = await bankUtil.preAuthorizePayment(schedIdAddress, this.maxDeployPayment, this.paymentAccountKeystore);\n const { dataRange, dataValues, dataPattern, sliceCount } = marshalInputData(this.jobInputData);\n if(dataValues)\n this.dataValues = dataValues;\n\n this.readyStateChange('deploying');\n\n /* Payload format is documented in scheduler-v4/libexec/job-submit/operations/submit.js */\n const submitPayload = {\n owner: myId.address,\n paymentAccount: this.paymentAccountKeystore.address,\n priority: 0, // @nyi\n\n workFunctionURI: this.workFunctionURI,\n uuid: this.uuid,\n mvMultSlicePayment: Number(this.feeStructure.marketValue) || 0, // @todo: improve feeStructure internals to better reflect v4\n absoluteSlicePayment: Number(this.feeStructure.maxPerRequest) || 0,\n requirePath: this.requirePath,\n dependencies: moduleDependencies,\n requirements: this.requirements, /* capex */\n localExec: this.inLocalExec,\n force100pctCPUDensity: this.force100pctCPUDensity,\n estimationSlices: this.estimationSlices,\n greedyEstimation: this.greedyEstimation,\n workerConsole: this.workerConsole,\n isCI: this.isCI,\n\n description: this.public.description || 'Discreetly making the world smarter',\n name: this.public.name || 'Ad-Hoc Job' + adhocId,\n link: this.public.link || '',\n\n preauthToken, // XXXwg/er @todo: validate this after fleshing out the stub(s)\n\n resultStorageType: this.resultStorageType, // @todo: implement other result types\n resultStorageDetails: this.resultStorageDetails, // Content depends on resultStorageType\n resultStorageParams: this.resultStorageParams, // Post params for off-prem storage\n dataRange,\n dataPattern,\n sliceCount\n };\n\n /* Determine thee type of the arguments option and set the submit message payload accordingly. */\n if (Array.isArray(this.jobArguments) && this.jobArguments.length === 1 && this.jobArguments[0] instanceof DcpURL)\n submitPayload.arguments = this.jobArguments[0].href;\n else if (this.jobArguments instanceof RemoteDataSet)\n submitPayload.marshaledArguments = kvinMarshal(this.jobArguments.map(e => new URL(e)))\n else if (this.jobArguments)\n {\n try\n {\n submitPayload.marshaledArguments = kvinMarshal(Array.from(this.jobArguments));\n }\n catch(e)\n {\n throw new Error(`Could not convert job arguments to Array (${e.message})`);\n }\n }\n \n if (this.inLocalExec && !DCP_ENV.isBrowserPlatform)\n {\n const workFunctionFile = createTempFile('dcp-localExec-workFunction-XXXXXXXXX', 'js');\n const argumentsFile = createTempFile('dcp-localExec-arguments-XXXXXXXXX', 'js');\n \n // For allowed origins of the localexec worker. Only allow the origins (files in this case) in this list.\n this.localExecAllowedFiles = [workFunctionFile.filename, argumentsFile.filename];\n\n // get the workFunctionURI string before writing to file to prevent the need to double-decode the work function in the worker.\n const workFunction = await fetchURI(this.workFunctionURI);\n workFunctionFile.writeSync(workFunction);\n \n const workFunctionFileURL = new URL('file://' + workFunctionFile);\n submitPayload.workFunctionURI = workFunctionFileURL.href;\n this.workFunctionURI = workFunctionFileURL.href;\n \n if (submitPayload.marshaledArguments)\n {\n argumentsFile.writeSync(JSON.stringify(submitPayload.marshaledArguments));\n const argumentsFileURL = new URL('file://' + argumentsFile.filename);\n submitPayload.marshaledArguments = kvinMarshal([argumentsFileURL]);\n }\n }\n\n // XXXpfr Excellent tracing.\n if (debugging('dcp-client'))\n {\n dumpObject(submitPayload, 'Submit: Job Index: submitPayload', 256);\n console.debug('Before Deploy', myId);\n }\n\n // Deploy the job! If we get an error, try again a few times until threshold of errors is reached, then actually throw it\n let deployed\n let deployAttempts = 0;\n while (deployAttempts++ < (dcpConfig.job.deployAttempts || 10))\n {\n try\n {\n deployed = await this.useDeployConnection('submit', submitPayload, myId);\n break;\n }\n catch (e)\n {\n if (deployAttempts < 10)\n debugging('dcp-client') && console.debug('Error when trying to deploy job, trying again', e);\n else\n throw e;\n }\n }\n\n if (!deployed.success)\n {\n // close all of the connections so that we don't cause node processes to hang.\n const handleErr = (e) => {\n console.error('Error while closing job connection:');\n console.error(e);\n };\n \n this.closeDeployConnection();\n this.eventSubscriber.close().catch(handleErr);\n computeGroups.closeServiceConnection().catch(handleErr);\n \n // Yes, it is possible for deployed or deployed.payload to be undefined.\n if (deployed.payload)\n {\n if (deployed.payload.code === 'ENOTFOUND')\n throw new DCPError(`Failed to submit job to scheduler. Account: ${submitPayload.paymentAccount} was not found or does not have sufficient balance (${deployed.payload.info.deployCost} DCCs needed to deploy this job)`, deployed.payload); \n throw new DCPError('Failed to submit job to scheduler', deployed.payload);\n }\n throw new DCPError('Failed to submit job to scheduler (no payload)', deployed ? deployed : '');\n }\n\n debugging('dcp-client') && console.debug('After Deploy', JSON.stringify(deployed));\n\n this.address = deployed.payload.job;\n this.deployCost = deployed.payload.deployCost;\n\n if (!this.status)\n this.status = {\n runStatus: null,\n total: 0,\n computed: 0,\n distributed: 0,\n };\n \n this.status.runStatus = deployed.payload.status;\n this.status.total = deployed.payload.lastSliceNumber;\n this.running = true;\n }\n \n /** close an open job to indicate we are done adding data so it is okay to finish\n * the job at the appropriate time\n */\n close ()\n {\n return this.useDeployConnection('closeJob', {\n job: this.id,\n });\n }\n \n /** Use the connection to job submit service. Will open a new connection if one does not exist,\n * and close the connection if it is idle for more than 10 seconds (tuneable).\n */\n useDeployConnection(...args)\n {\n if (!this.deployConnection)\n {\n this.deployConnection = new protocolV4.Connection(dcpConfig.scheduler.services.jobSubmit); \n this.deployConnection.on('close', () => { this.deployConnection = null; });\n }\n if (this.deployConnectionTimeout)\n clearTimeout(this.deployConnectionTimeout);\n \n const deployPromise = this.deployConnection.send(...args);\n \n this.deployConnectionTimeout = setTimeout(() => this.deployConnection.close(), (dcpConfig.job.deployCloseTimeout || 10 * 1000));\n if (!ON_BROWSER)\n this.deployConnectionTimeout.unref();\n \n return deployPromise;\n }\n \n /**\n * Close the connection to the job submit (if it exists), and clear the close timeout (if needed).\n */\n closeDeployConnection()\n {\n if (this.deployConnection)\n this.deployConnection.close();\n if (this.deployConnectionTimeout)\n clearTimeout(this.deployConnectionTimeout);\n }\n}\n\n/**\n * Depending on the shape of the job's data, resolve it into a RangeObject, a\n * Pattern, or a values array, and return it in the appropriate property.\n *\n * @param {any} data Job's input data\n * @return {MarshaledInputData} An object with one of the following properties set:\n * - dataValues: job input is an array of arbitrary values \n * - dataPattern: job input is a URI pattern \n * - dataRange: job input is a RangeObject (and/or friends)\n */\nfunction marshalInputData (data)\n{\n if (!(data instanceof Object || data instanceof SuperRangeObject))\n throw new TypeError(`Invalid job data type: ${typeof data}`);\n\n /**\n * @type MarshaledInputData\n */\n const marshalledInputData = {};\n\n // TODO(wesgarland): Make this more robust.\n if (data instanceof SuperRangeObject ||\n (data.hasOwnProperty('ranges') && data.ranges instanceof MultiRangeObject) ||\n (data.hasOwnProperty('start') && data.hasOwnProperty('end')))\n marshalledInputData.dataRange = data;\n else if (Array.isArray(data))\n marshalledInputData.dataValues = data;\n else if (data instanceof URL || data instanceof DcpURL)\n marshalledInputData.dataPattern = String(data);\n else if(data instanceof RemoteDataSet)\n marshalledInputData.dataValues = data.map(e => new URL(e));\n else if(data instanceof RemoteDataPattern)\n {\n marshalledInputData.dataPattern = data['pattern'];\n marshalledInputData.sliceCount = data['sliceCount'];\n }\n\n log('marshalledInputData:', marshalledInputData);\n return marshalledInputData;\n}\n\n/**\n * marshal the value using kvin or instance of the kvin (tunedKvin)\n * tunedKvin is defined if job.tuning.kvin is specified.\n *\n * @param {any} value \n * @return {object} A marshaled object\n * \n */\nfunction kvinMarshal (value) {\n if (tunedKvin)\n return tunedKvin.marshal(value);\n\n return kvin.marshal(value);\n}\n\n\n\nexports.Job = Job;\nexports.SlicePaymentOffer = SlicePaymentOffer;\nexports.ResultHandle = ResultHandle;\n\n\n//# sourceURL=webpack://dcp/./src/dcp-client/job/index.js?");
4220
+ eval("/**\n * @file job/index.js\n * @author Eddie Roosenmaallen, eddie@kingsds.network\n * Matthew Palma, mpalma@kingsds.network\n * Wes Garland, wes@kingsds.network\n * Paul, paul@kingsds.network\n * Ryan Saweczko, ryansaweczko@kingsds.network\n * @date November 2018\n * November 2018\n * February 2022\n * May 2022\n * Jun 2022\n *\n * This module implements the Compute API's Job Handle\n *\n */\n/** @typedef {import('dcp/dcp-client/wallet/keystore').Keystore} Keystore */\n\n\nconst { BigNumber } = __webpack_require__(/*! bignumber.js */ \"./node_modules/bignumber.js/bignumber.js\");\nconst { v4: uuidv4 } = __webpack_require__(/*! uuid */ \"./node_modules/uuid/dist/esm-browser/index.js\");\nconst { EventEmitter, PropagatingEventEmitter } = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\");\nconst { RangeObject, MultiRangeObject, DistributionRange, SuperRangeObject, SparseRangeObject } = __webpack_require__(/*! dcp/dcp-client/range-object */ \"./src/dcp-client/range-object.js\");\nconst { fetchURI, encodeDataURI, 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('../range-object').RangeLike} RangeLike */\n\n/**\n * Ensure input data is an appropriate format\n * @param {RangeObject | DistributionRange | RemoteDataSet | Array | Iterable}\n * inputData - A URI-shaped string, a [Multi]RangeObject-constructing value, or\n * an array of slice data\n * @return {RangeObject | RangeLike | DistributionRange | RemoteDataSet | Array}\n * The coerced input in an appropriate format ([Multi]RangeObject,\n * DistributionRange, RemoteDataSet, or array)\n */\n const wrangleData = (inputData) => {\n\n if (RangeObject.isRangelike(inputData)) { return inputData }\n if (RangeObject.isRangeObject(inputData)) { return inputData }\n if (DistributionRange.isDistribution(inputData)) { return inputData }\n if (inputData instanceof SparseRangeObject) { return inputData }\n if (inputData instanceof MultiRangeObject) { return inputData }\n if (MultiRangeObject.isProtoMultiRangelike(inputData)) { return new MultiRangeObject(inputData) }\n if (RangeObject.isProtoRangelike(inputData)) { return new RangeObject(inputData) }\n if (DistributionRange.isProtoDistribution(inputData)) { return new DistributionRange(inputData) }\n if (RemoteDataSet.isRemoteDataSet(inputData)) { return inputData }\n if (RemoteDataPattern.isRemoteDataPattern(inputData)) { return inputData }\n\n return Array.isArray(inputData) ? inputData : [inputData];\n};\n\n/**\n * @classdesc The Compute API's Job Handle (see {@link https://docs.dcp.dev/specs/compute-api.html#job-handles|Compute API spec})\n * Job handles are objects which correspond to jobs. \n * They are created by some exports of the compute module, such as {@link module:dcp/compute.do|compute.do} and {@link module:dcp/compute.for|compute.for}.\n * @extends module:dcp/dcp-events.PropagatingEventEmitter\n * @hideconstructor\n * @access public\n */\nclass Job extends PropagatingEventEmitter\n{\n /**\n * Fired when the job is accepted by the scheduler on deploy.\n * \n * @event Job#accepted\n * @access public\n * @type {object}\n *//**\n * Fired when the job is cancelled.\n * \n * @event Job#cancel\n * @access public\n *//**\n * Fired when a result is returned.\n * \n * @event Job#result\n * @access public\n * @type {object}\n * @property {string} jobAddress Address of the job\n * @property {string} task ID of the task (slice) the result came from\n * @property {number} sort The index of the slice\n * @property {object} result\n * @property {string} result.request\n * @property {*} result.result The value returned from the work function\n *//**\n * Fired when the result handle is modified, either when a new `result` event is fired or when the results are populated with `results.fetch()`\n * \n * @event Job#resultsUpdated\n * @access public\n *//**\n * Fired when the job has been completed.\n * \n * @event Job#complete\n * @access public\n * @type {ResultHandle}\n *//**\n * Fired when the job's status changes.\n * \n * @event Job#status\n * @access public\n * @type {object}\n * @property {string} jobAddress Address of the job\n * @property {number} total Total number of slices in the job\n * @property {number} distributed Number of slices that have been distributed\n * @property {number} computed Number of slices that have completed execution (returned a result)\n * @property {string} runStatus Current runStatus of the job\n *//**\n * Fired when a slice throws an error.\n * \n * @event Job#error\n * @access public\n * @type {object}\n * @property {string} jobAddress Address of the job\n * @property {number} sliceIndex Index of the slice that threw the error\n * @property {string} message The error message\n * @property {string} stack The error stacktrace\n * @property {string} name The error type name\n *//**\n * Fired when a slice uses one of the console log functions.\n * \n * @event Job#console\n * @access public\n * @type {object}\n * @property {string} jobAddress Address of the job\n * @property {number} sliceIndex The index of the slice that produced this event\n * @property {string} level The log level, one of `debug`, `info`, `log`, `warn`, or `error`\n * @property {string} message The console log message\n *//**\n * Fired when a slice is stopped for not calling progress. Contains information about how long the slice ran for, and about the last reported progress calls.\n * \n * @event Job#noProgress\n * @access public\n * @type {object}\n * @property {string} jobAddress Address of the job\n * @property {number} sliceIndex The index of the slice that failed due to no progress\n * @property {number} timestamp How long the slice ran before failing\n * @property {object} progressReports\n * @property {object} progressReports.last The last progress report received from the worker\n * @property {number} progressReports.last.timestamp Time since the start of the slice\n * @property {number} progressReports.last.progress Progress value reported\n * @property {*} progressReports.last.value The last value that was passed to the progress function\n * @property {number} progressReports.last.throttledReports Number of calls to progress that were throttled since the last report\n * @property {object} progressReports.lastUpdate The last determinate (update to the progress param) progress report received from the worker\n * @property {number} progressReports.lastUpdate.timestamp\n * @property {number} progressReports.lastUpdate.progress\n * @property {*} progressReports.lastUpdate.value\n * @property {number} progressReports.lastUpdate.throttledReports\n *//**\n @todo: is this in the spec? is there a no progress data? should there be?\n * Identical to `noProgress`, except that it also contains the data that the slice was executed with.\n * \n * @event Job#noProgressData\n * @access public\n * @type {object}\n * @property {*} data The data that the slice was executed with\n *//**\n * Fired when the job is paused due to running out of funds. The job can be resumed by escrowing more funds then resuming the job.\n * @todo: is this a thing, should it be a thing (the payload)\n * Event payload is the estimated funds required to complete the job\n * \n * @event Job#nofunds\n * @access public\n * @type {BigNumber}\n *//**\n * Fired when the job cannot be deployed due to no bank account / not enough balance to deploy the job\n * \n * @event Job#ENOFUNDS\n * @access public\n *//**\n * Fired when the job is cancelled due to the work function not calling the `progress` method frequently enough.\n * \n * @event Job#ENOPROGRESS\n * @access public\n *//**\n * The job was cancelled because scheduler has determined that individual tasks in this job exceed the maximum allowable execution time.\n * \n * @event Job#ESLICETOOSLOW\n * @access public\n *//**\n * Fired when the job is cancelled because too many work functions are terminating with uncaught exceptions.\n * \n * @event Job#ETOOMANYERRORS\n * @access public\n */\n\n /**\n * @form1 new Job('application_worker_address'[, data[, arguments]])\n * @form2a new Job('worker source'[, data[, arguments]])\n * @form2b new Job(worker_function[, data[, arguments]])\n */\n constructor ()\n {\n super('Job');\n if (typeof arguments[0] === 'function')\n arguments[0] = arguments[0].toString();\n\n if (typeof arguments[0] === 'string')\n {\n const { encodeDataURI } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\n this.workFunctionURI = encodeDataURI(arguments[0], 'application/javascript');\n } \n else if (DcpURL.isURL(arguments[0]))\n this.workFunctionURI = arguments[0].href;\n\n this.jobInputData = wrangleData(arguments[1] || []);\n this.jobArguments = wrangleData(arguments[2] || []);\n \n log('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 number (can be null, undefined, or infinity) describing the estimationSlicesRemaining in the jpd (dcp-2593)\n * @type {number}\n * @access public\n */\n this.estimationSlices = undefined;\n /**\n * When true, allows a job in estimation to have requestTask return multiple estimation slices.\n * This flag applies independent of infinite estimation, viz., this.estimationSlices === null .\n * @type {boolean}\n * @access public\n */\n this.greedyEstimation = false;\n /**\n * tunable parameters per job\n * @access public\n * @param {object} tuning \n * @param {string} tuning.kvin Encode the TypedArray into a string, trying multiple methods to determine optimum \n * size/performance. The this.tune variable affects the behavior of this code this:\n * @param {boolean} speed If true, only do naive encoding: floats get represented as byte-per-digit strings\n * @param {boolean} size If true, try the naive, ab8, and ab16 encodings; pick the smallest\n * If both are false try the naive encoding if under typedArrayPackThreshold and use if smaller\n * than ab8; otherwise, use ab8\n */\n this.tuning = {\n kvin: {\n size: false,\n speed: false,\n },\n }\n /* For API interface to end-users only */\n Object.defineProperty(this, 'id', {\n get: () => this.address,\n set: (id) => { this.address = id }\n });\n \n this.uuid = uuidv4(); /** @see {@link https://kingsds.atlassian.net/browse/DCP-1475?atlOrigin=eyJpIjoiNzg3NmEzOWE0OWI4NGZkNmI5NjU0MWNmZGY2OTYzZDUiLCJwIjoiaiJ9|Jira Issue} */\n this.dependencies = []; /* dependencies of the work function */\n this.requirePath = []; /* require path for dependencies */\n this.connected = false; /* true when exec or resume called */\n this.results = new ResultHandle(this); /* result handle */\n this.collateResults = true; /* option to receive results as they are computed & ensure all are received on finish */\n this.force100pctCPUDensity = false; /* tell scheduler to assume this job uses 100% cpu density */\n this.workerConsole = false; /* tell workers to log more information about failures in the evaluator this job causes */\n this.address = null; /* job address, created by scheduler during exec call. */\n this.paymentAccountKeystore = null; /* keystore for payment for job to come from */\n this.status = { /* job status details */\n runStatus: null,\n total: null,\n distributed: null,\n computed: null\n };\n // Compute groups. Add to public compute group by default\n this.computeGroups = [ Object.assign({}, schedulerConstants.computeGroups.public) ];\n \n // Update the ready state as we go through job deployment\n this.readyState = sliceStatus.new;\n const that = this;\n this.readyStateChange = function job$$readyStateChange (readyState)\n {\n that.readyState = readyState;\n that.emit('readyStateChange', that.readyState);\n }\n }\n \n /**\n * Initialize the various event systems the job handle requires. These include:\n * - an internal event emitter (this.ee)\n * - an event emitter for any events emitted on `work.emit` within work functions (this.work)\n * - an event subscriber to subscribe (to receive) events from the scheduler (this.eventSubscriber)\n */\n initEventSystems ()\n {\n // Handle the various event-related things required in the constructor\n\n // Internal event emitter for events within job handle\n this.ee = new EventEmitter('Job Internal');\n\n /**\n * An EventEmitter for custom events dispatched by the work function.\n * @type {module:dcp/dcp-events.EventEmitter}\n * @access public\n * @example\n * // in work function\n * work.emit('myEventName', 1, [2], \"three\");\n * // client-side\n * job.work.on('myEventName', (num, arr, string) => { });\n */\n this.work = new EventEmitter('job.work');\n this.listenForCustomEvents = false;\n\n // 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 receipt = response.payload;\n return receipt;\n }\n\n /**\n * create bundles for local dependencies\n */\n _pack ()\n {\n var retval = (__webpack_require__(/*! ./node-modules */ \"./src/dcp-client/job/node-modules.js\").createModuleBundle)(this.dependencies);\n return retval;\n }\n\n /** \n * Collect all of the dependencies together, throw them into a BravoJS\n * module which sideloads them as a side effect of declaration, and transmit\n * them to the package manager. Then we return the package descriptor object,\n * which is guaranteed to have only one file in it.\n *\n * @returns {object} with properties name and files[0]\n */\n async _publishLocalModules()\n {\n const 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.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 this.marshaledArguments = [];\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 this.marshaledArguments.push(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 this.marshaledArguments.push(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 this.marshaledArguments.push(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.marshaledArguments.push(new URL('file://' + localArgFile.filename));\n this.localExecAllowedFiles['fetchArguments'].push(localArgFile.filename);\n }\n }\n } \n }\n }\n this.marshaledArguments = kvinMarshal(this.marshaledArguments);\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).then(() => this.close());\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 {\n submitPayload.marshaledArguments = [];\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 submitPayload.marshaledArguments = kvinMarshal(this.jobArguments.map(e => new URL(e)))\n else\n {\n for (let i = 0; i < this.jobArguments.length; i++)\n {\n if (this.jobArguments[i] instanceof URL)\n submitPayload.marshaledArguments.push(this.jobArguments[i])\n else\n {\n if (this.jobArguments[i] instanceof RemoteDataSet) /* Member of set is RemoteDataSet */\n this.jobArguments[i].forEach((e) => { submitPayload.marshaledArguments.push(new URL(e)) });\n else /* Actual Value */\n submitPayload.marshaledArguments.push(this.jobArguments[i]);\n }\n }\n submitPayload.marshaledArguments = kvinMarshal(submitPayload.marshaledArguments);\n }\n }\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 this.deployConnection = new protocolV4.Connection(dcpConfig.scheduler.services.jobSubmit); \n this.deployConnection.on('close', () => { this.deployConnection = null; });\n }\n if (this.deployConnectionTimeout)\n clearTimeout(this.deployConnectionTimeout);\n \n const deployPromise = this.deployConnection.send(...args);\n \n deployPromise.finally(() => {\n if (this.useDeployConnection.uses === 1)\n {\n this.deployConnectionTimeout = setTimeout(() => {\n this.useDeployConnection.uses === 0 && this.deployConnection && this.deployConnection.close()\n }, (dcpConfig.job.deployCloseTimeout || 10 * 1000));\n if (!ON_BROWSER)\n this.deployConnectionTimeout.unref();\n }\n this.useDeployConnection.uses--;\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?");
4172
4221
 
4173
4222
  /***/ }),
4174
4223
 
@@ -4179,7 +4228,7 @@ eval("/**\n * @file job/index.js\n * @author Eddie Roosenmaallen, ed
4179
4228
  /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
4180
4229
 
4181
4230
  "use strict";
4182
- eval("/**\n * @file node-modules.js Node-specific support for sending jobs to the scheduler\n * which use modules supplied by the local filesystem.\n * @author Wes Garland, wes@kingsds.network\n * @date May 2020\n */\n\nconst { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\nconst utils = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\nconst process = requireNative('process');\nconst path = requireNative('path');\nconst os = requireNative('os');\nconst fs = requireNative('fs');\nconst pathPrivacyMode = true; /** @todo XXX tie into job, false for localExec(?) */\nconst debugging = (__webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope)('job-modules');\n\nif (!debugging() && exports.debug) {\n debugging = function nodeModules$$debugging() { return true };\n}\n\n/** \n * Fix windows backslashes, which are a problem when re-interpreted by functions like fs.open() \n * (because they are interpreted as escapes). This \"fix\" changes them from backslashes to forward\n * slashes, which node (and hench webpack) errorneously interpret as directory separators on win32.\n * \n * Doing this fix every time we get a path from the OS (eg via path or require.resolve) means we \n * can mostly treat windows paths, unix paths, and commonjs paths the same way everywhere, with the\n * notable except of absolute paths like c:/path/to/module.js - these must be re-written when used\n * as part of a canonical module identifier (into /mnt/c/path/to/module.js), and must be detected\n * via path.isAbsolute() rather than looking at the first character of the path.\n */\nfunction fixSlashes(diskPath) {\n diskPath = os.platform() === 'win32' ? diskPath.replace(/[\\\\]/g, '/') : diskPath;\n return diskPath;\n}\n\n/** Create a module bundle in the form of a BravoJS module which, when evaluated, injects a module bunch of\n * modules into the running BravoJS environment. The module bundles are currently created for NodeJS via\n * webpack, but that is an implementation detail.\n *\n * @param ${requireList} {string | Array} module identifier or list of module identifiers\n * @param buildMode {string | undefined} 'debug' for source maps or 'release' for minified\n * @param outerRequrire {string | undefined} require function to use to resolve module identifiers.\n * If undefined, resolve relative to program module.\n *\n * @todo This needs to be changed to use async calls for the spawned process (maintaining stdio mappings!) \n * and the file copying operations, so that we can deploy the job at the same time as the I/O heavy\n * stuff is happening on the local box. /wg may 2020\n */\nexports.createModuleBundle = function job$$createModuleBundle(requireList, buildMode, outerRequire) {\n const webpack = requireNative('webpack');\n const child_process = requireNative('child_process');\n \n var configuration;\n var webpackOutput, webpackInput, webpackConfig, finalOutput;\n var unresolved = []; /* list of module identifiers which could not be resolved (hopefully are deployed on network) */\n var bundlePathMap = {}; /* table mapping canonical id to real path. Usually similar, not always (thanks npm) */\n var resolveMap = {}; /* table mapping module identifier to canonical id */\n var searchPaths = {}; /* table (that becomes a list) enumerating searchable directories discovered during resolution */\n var mainModuleDir, modulePaths;\n var child;\n var args = [];\n var prologue, epilogue, md5Stream;\n\n if (!buildMode)\n buildMode = 'debug';\n if (!outerRequire)\n {\n const moduleModule = requireNative('module');\n const createRequire = moduleModule.createRequire || moduleModule.createRequireFromPath;\n outerRequire = createRequire(process.argv[1]);\n }\n\n mainModuleDir = canonicalize(fixSlashes(path.dirname(outerRequire.main.filename)), outerRequire, null, []);\n modulePaths = outerRequire.main.paths;\n\n for (let moduleIdentifier of requireList) {\n let canonicalPath = canonicalize(moduleIdentifier, outerRequire, mainModuleDir, modulePaths);\n if (!canonicalPath) {\n unresolved.push(moduleIdentifier);\n continue;\n }\n bundlePathMap[canonicalPath] = moduleIdentifier;\n resolveMap[moduleIdentifier] = canonicalPath;\n if (canonicalPath.startsWith('/localhost/program/node_modules/') || canonicalPath.startsWith('/localhost/search/node_modules/')) {\n let npmDir = canonicalPath.split('/').slice(0,5).join('/');\n searchPaths[npmDir] = true;\n } else if (canonicalPath.startsWith('/localhost/search/')) {\n let npmDir = canonicalPath.split('/').slice(0,4).join('/');\n searchPaths[npmDir] = true;\n }\n }\n if (Object.keys(bundlePathMap).length === 0) {\n return { unresolved };\n }\n \n /* Create the module bundle, exporting the modules that will be packed into it \n * and annotating the effects of search path expansion package.json magic.\n */\n try {\n webpackOutput = utils.createTempFile(`dcp-${process.pid}-module-bundle-XXXXXXXX`, 'js').closeSync();\n webpackInput = utils.createTempFile(`dcp-${process.pid}-module-index-XXXXXXXX`, 'js');\n webpackConfig = utils.createTempFile(`dcp-${process.pid}-module-config-XXXXXXXX`, 'js');\n finalOutput = utils.createTempFile(`dcp-${process.pid}-final-bundle-XXXXXXXX`, 'js');\n\n searchPaths = Object.keys(searchPaths);\n webpackInput.writeSync('Object.assign(exports, {\\n');\n for (let moduleId in bundlePathMap) {\n let diskPath = fixSlashes(outerRequire.resolve(bundlePathMap[moduleId]));\n webpackInput.writeSync(` \"${moduleId}\": require(\"${diskPath}\"),\\n`);\n }\n \n webpackInput.writeSync('});\\n');\n webpackInput.writeSync(`defineSearchPaths(${JSON.stringify(searchPaths)});\\n`);\n webpackInput.writeSync(`defineResolveMap(${JSON.stringify(resolveMap)});\\n`);\n /* Run webpack in a separate process to work around issues with require()ing it\n * from a local git repo via npm which is not this git repo. The output of the\n * webpack is the module bundle, in amd define() transport format.\n */\n configuration = {\n mode: buildMode === 'debug' ? 'development' : 'release',\n optimization: { minimize: buildMode === 'debug' },\n entry: webpackInput.filename,\n output: {\n path: path.dirname(webpackOutput.filename),\n filename: path.basename(webpackOutput.filename),\n libraryTarget: 'amd',\n library: 'dcp-localhost',\n },\n }\n\n switch(parseInt(webpack.version, 10))\n {\n case 0: case 1: case 2: case 3:\n throw new Error('Webpack ' + webpack.version + ' is not supported');\n case 4:\n {\n console.warn('Warning: webpack 4 is deprecated - please upgrade!');\n // configuration.output.ecmaVersion = 6;\n configuration.output.pathinfo = buildMode === 'debug';\n\n // Remove fs module from generated bundle\n configuration.node = { fs: 'empty' };\n\n if (!process.env.DCP_DEBUG_WEBPACK)\n {\n args.push('--log-level=warn');\n }\n break;\n }\n default:\n console.warn('Warning: webpack ' + webpack.version + ' not officially supported');\n /* fallthrough */\n case 5:\n {\n configuration.output.pathinfo = buildMode === 'debug';\n configuration.target = [ 'es6' ];\n\n // Need to set a default script chunk format based on the target.\n configuration.output.chunkFormat = 'commonjs';\n\n configuration.resolve = {\n fallback: {\n // Remove fs module from generated bundle\n fs: false,\n }\n };\n break;\n }\n }\n\n args[0] = process.execPath;\n args[1] = fixSlashes(path.resolve(path.dirname(requireNative.resolve('webpack')), '..', 'bin', 'webpack.js'));\n\n webpackConfig.writeSync('module.exports=' + JSON.stringify(configuration));\n args.push(`--config=${webpackConfig.filename}`);\n\n if (dcpConfig.job && dcpConfig.job.modules && dcpConfig.job.modules.webpackOptions)\n args = args.concat(utils.tokenize(dcpConfig.job.modules.webpackOptions));\n if (process.env.DCP_NODE_MODULES_WEBPACK_OPTIONS)\n args = args.concat(utils.tokenize(process.env.DCP_NODE_MODULES_WEBPACK_OPTIONS));\n\n debugging() && console.log('Creating job.requires(dependencies) bundle:', args);\n child = child_process.spawnSync(args[0], args.slice(1), {\n shell: false,\n windowsHide: true,\n stdio: 'inherit',\n });\n\n if (child.status !== 0)\n throw new Error(`Child process returned exit code ${child.status}`);\n \n /* Add a wrapper function around the webpack output which turns it into a valid\n * CommonJS Modules/2.0-draft8 (eg BravoJS) module that does not pollute scope.\n * Calculate the while we're at it to avoid double-reading large files.\n */\n ({prologue, epilogue} = sideloadGenerator());\n md5Stream = __webpack_require__(/*! create-hash */ \"./node_modules/create-hash/browser.js\")('md5');\n finalOutput.writeSync(prologue);\n md5Stream.write(prologue);\n utils.catFile(webpackOutput, finalOutput, md5Stream);\n finalOutput.writeSync(epilogue);\n md5Stream.write(epilogue);\n md5Stream.end();\n } finally {\n webpackInput.closeSync();\n webpackConfig.closeSync();\n finalOutput.closeSync();\n\n webpackInput.remove();\n webpackConfig.remove();\n webpackOutput.remove();\n }\n\n return {\n tempFile: finalOutput,\n hash: md5Stream.read(),\n unresolved: unresolved,\n }\n}\n\n/** \n * Inject the modules in a module bundle into the current BravoJS environment's\n * internal module memo, after invoking module.declare() so that we can masquerade \n * as a simple module dependency.\n *\n * @note This function exists in source form so that it isn't annoying to edit. \n * It gets used by being converted to string and parsed by sideloadGenerator().\n * The line containing the define(\"webpack...\") string must not be changed, \n * as it is a parse token.\n *\n * Additionally, all newlines will be eliminated to preserve the webpack source\n * map, so no C++-style comments should be used, and semicolons are mandatory.\n * Whitespace is also compacted, even inside string literals.\n */\nconst sideloader = `function sideloader() {\n module.declare(function(){});\n (function IIFE() { /* inject webpacked module group into BravoJS' module memo */\n /* Add program module paths discovered during bundling to global require path */\n function defineSearchPaths(searchPaths) {\n for (path of searchPaths)\n bravojs.paths.push(path);\n }\n /* Add name mapping for requires args to cover cases where resolution happens in package.json */\n function defineResolveMap(pathMap) {\n if (!bravojs.resolveMap)\n bravojs.resolveMap = {};\n bravojs.resolveMap = Object.assign(bravojs.resolveMap, pathMap);\n if (!bravojs.globalResolveHook) {\n bravojs.globalResolveHook = function dcp$$define$resolve(moduleIdentifier) {\n if (bravojs.resolveMap.hasOwnProperty(moduleIdentifier))\n return bravojs.resolveMap[moduleIdentifier];\n else\n return moduleIdentifier;\n }\n }\n }\n function define(amdModuleLabel, amdModuleDeps, amdModuleFactory) {\n var moduleGroup = amdModuleFactory();\n\n for (let moduleIdentifier in moduleGroup) {\n if (!moduleGroup.hasOwnProperty(moduleIdentifier))\n continue;\n bravojs.requireMemo[moduleIdentifier] = moduleGroup[moduleIdentifier];\n if (bravojs.debug)\n console.log(\\`Memoized module \\${moduleIdentifier}: \\${Object.keys(moduleGroup[moduleIdentifier])}\\`);\n }\n if (bravojs.mainModule !== '/localhost/program') {\n bravojs.mainModuleDir = '/localhost/program';\n bravojs.global.require = bravojs.requireFactory(bravojs.mainModuleDir);\n }\n }\n define(\"webpack amd output here\", [], factoryFunction);\n })();\n}`;\n\n/** Generate the prologue and epilogue for a webpack in amd library mode which, when \n * all three parts are joined, will sideload the webpack's exports into BravoJS'\n * running require memo, exposing them as require() modules.\n */\nfunction sideloadGenerator() {\n var slInner = sideloader.toString().replace(/^[^{]*{/,'').replace(/} *$/,'');\n var offset_define = slInner.indexOf('define(\"webpack amd output here\"');\n var offset_epilogue = slInner.slice(offset_define).indexOf('\\n');\n var prologue, epilogue;\n \n if (offset_define === -1 || offset_epilogue === -1) {\n throw new Error(`sideload function missing parse token(s)`);\n }\n\n prologue = slInner.slice(0, offset_define) .replace(/[ \\n\\r\\t]+/g, ' ').replace(/(^ | $)/g,'');\n epilogue = slInner.slice(offset_define + offset_epilogue).replace(/[ \\n\\r\\t]+/g, ' ').replace(/(^ | $)/g,'');\n\n return {prologue, epilogue};\n}\n\n/** Return the canonical module id for a given module. All of the modules begin\n * begin in the localhost directory off the conceptual module system root, as these\n * are \"local\" -- not deployed -- modules. Otherwise, they mostly represent the\n * structure on disk, although this is explicitly a path within the conceptual module\n * system space and not actually any physical filesystem.\n * \n * In \"release\" mode, we further modify the canonical path to preserve privacy\n * wrt the developer's disk layout: All search-path directories are \"collapsed\" \n * onto a single directory, and the program-module directory is anonymized. In\n * that case, the program module's \"directory\" is /localhost/program/ and the\n * search path is /localhost/search/.\n *\n * @param {string} moduleIdentifier a module identifier (argument to require()) or absolute path to module\n * @param {function} outerRequire the require() function used by the node code (not the work function)\n * @param {string} mainModuleDir the directory of the main (program) module; used to calculate safety\n * of relative paths when in path-privacy mode. Passing excatly null\n * disables this path-privacy mode.\n * @param {Array} modulePaths List of paths to search (like require.paths in CJS)\n */\nfunction canonicalize(moduleIdentifier, outerRequire, mainModuleDir, modulePaths) {\n var filename, fno;\n var canonical = '';\n var doPathPrivacy = pathPrivacyMode && mainModuleDir !== null;\n \n try {\n filename = (path.isAbsolute(moduleIdentifier) ? path : outerRequire).resolve(moduleIdentifier);\n fixSlashes(filename);\n } catch(e) {\n if (moduleIdentifier.startsWith('./') ||\n moduleIdentifier.startsWith('../') ||\n moduleIdentifier.startsWith('/') ||\n path.isAbsolute(moduleIdentifier)) {\n throw new Error(`Cannot locate module '${moduleIdentifier}'`);\n }\n debugging() && console.warn(`Cannot locate module ${moduleIdentifier}; assuming on deployed search path`);\n return null;\n }\n\n fno = path.parse(filename);\n if (os.platform() === 'win32' && fno.root.match(/[A-Za-z]:/)) {\n /* put drive letters into a virtual mount point */\n let newDir = '/mnt/' + fno.root[0].toLowerCase();\n if (fno.dir != fno.root)\n newDir += '/' + fno.dir.slice(fno.root.length);\n fno.root = '/';\n fno.dir = newDir;\n }\n /* root is always /\n * dir always starts with a / unless its empty and does not end in a /\n * base never starts with a / and does not end in a /\n */ \n canonical = fno.root;\n if (fno.dir)\n canonical += fno.dir.slice(fno.root.length);\n canonical += '/' + fno.name;\n\n if (doPathPrivacy) {\n let mmRE = new RegExp(`^${mainModuleDir}(/|$)`);\n if (canonical.match(mmRE)) {\n canonical = '/program' + canonical.slice(mainModuleDir.length);\n } else {\n if (moduleIdentifier.startsWith('./') || moduleIdentifier.startsWith('../')) {\n throw new Error(`Relative module identifiers above the program module's directory are not supported in path privacy mode (${canonical})`);\n }\n canonical = '/search' + moduleIdentifier;\n }\n }\n\n if (doPathPrivacy)\n return '/localhost' + canonical;\n else\n return canonical;\n}\n\n\n//# sourceURL=webpack://dcp/./src/dcp-client/job/node-modules.js?");
4231
+ eval("/**\n * @file node-modules.js Node-specific support for sending jobs to the scheduler\n * which use modules supplied by the local filesystem.\n * @author Wes Garland, wes@kingsds.network\n * @date May 2020\n */\n\nconst { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\nconst utils = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\nconst process = requireNative('process');\nconst path = requireNative('path');\nconst os = requireNative('os');\nconst fs = requireNative('fs');\nconst pathPrivacyMode = true; /** @todo XXX tie into job, false for localExec(?) */\nconst debugging = (__webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope)('job-modules');\n\nif (!debugging() && exports.debug) {\n debugging = function nodeModules$$debugging() { return true };\n}\n\n/** \n * Fix windows backslashes, which are a problem when re-interpreted by functions like fs.open() \n * (because they are interpreted as escapes). This \"fix\" changes them from backslashes to forward\n * slashes, which node (and hench webpack) errorneously interpret as directory separators on win32.\n * \n * Doing this fix every time we get a path from the OS (eg via path or require.resolve) means we \n * can mostly treat windows paths, unix paths, and commonjs paths the same way everywhere, with the\n * notable except of absolute paths like c:/path/to/module.js - these must be re-written when used\n * as part of a canonical module identifier (into /mnt/c/path/to/module.js), and must be detected\n * via path.isAbsolute() rather than looking at the first character of the path.\n */\nfunction fixSlashes(diskPath) {\n diskPath = os.platform() === 'win32' ? diskPath.replace(/[\\\\]/g, '/') : diskPath;\n return diskPath;\n}\n\n/** Create a module bundle in the form of a BravoJS module which, when evaluated, injects a module bunch of\n * modules into the running BravoJS environment. The module bundles are currently created for NodeJS via\n * webpack, but that is an implementation detail.\n *\n * @param ${requireList} {string | Array} module identifier or list of module identifiers\n * @param buildMode {string | undefined} 'debug' for source maps or 'release' for minified\n * @param outerRequrire {string | undefined} require function to use to resolve module identifiers.\n * If undefined, resolve relative to program module.\n *\n * @todo This needs to be changed to use async calls for the spawned process (maintaining stdio mappings!) \n * and the file copying operations, so that we can deploy the job at the same time as the I/O heavy\n * stuff is happening on the local box. /wg may 2020\n */\nexports.createModuleBundle = function job$$createModuleBundle(requireList, buildMode, outerRequire) {\n const webpack = requireNative('webpack');\n const child_process = requireNative('child_process');\n \n var configuration;\n var webpackOutput, webpackInput, webpackConfig, finalOutput;\n var unresolved = []; /* list of module identifiers which could not be resolved (hopefully are deployed on network) */\n var bundlePathMap = {}; /* table mapping canonical id to real path. Usually similar, not always (thanks npm) */\n var resolveMap = {}; /* table mapping module identifier to canonical id */\n var searchPaths = {}; /* table (that becomes a list) enumerating searchable directories discovered during resolution */\n var mainModuleDir, modulePaths;\n var child;\n var args = [];\n var prologue, epilogue, md5Stream;\n\n if (!buildMode)\n buildMode = 'debug';\n if (!outerRequire)\n {\n const moduleModule = requireNative('module');\n const createRequire = moduleModule.createRequire || moduleModule.createRequireFromPath;\n outerRequire = createRequire(process.argv[1]);\n }\n\n mainModuleDir = canonicalize(fixSlashes(path.dirname(outerRequire.main.filename)), outerRequire, null, []);\n modulePaths = outerRequire.main.paths;\n\n for (let moduleIdentifier of requireList) {\n let canonicalPath = canonicalize(moduleIdentifier, outerRequire, mainModuleDir, modulePaths);\n if (!canonicalPath) {\n unresolved.push(moduleIdentifier);\n continue;\n }\n bundlePathMap[canonicalPath] = moduleIdentifier;\n resolveMap[moduleIdentifier] = canonicalPath;\n if (canonicalPath.startsWith('/localhost/program/node_modules/') || canonicalPath.startsWith('/localhost/search/node_modules/')) {\n let npmDir = canonicalPath.split('/').slice(0,5).join('/');\n searchPaths[npmDir] = true;\n } else if (canonicalPath.startsWith('/localhost/search/')) {\n let npmDir = canonicalPath.split('/').slice(0,4).join('/');\n searchPaths[npmDir] = true;\n }\n }\n if (Object.keys(bundlePathMap).length === 0) {\n return { unresolved };\n }\n \n /* Create the module bundle, exporting the modules that will be packed into it \n * and annotating the effects of search path expansion package.json magic.\n */\n try {\n webpackOutput = utils.createTempFile(`dcp-${process.pid}-module-bundle-XXXXXXXX`, 'js').closeSync();\n webpackInput = utils.createTempFile(`dcp-${process.pid}-module-index-XXXXXXXX`, 'js');\n webpackConfig = utils.createTempFile(`dcp-${process.pid}-module-config-XXXXXXXX`, 'js');\n finalOutput = utils.createTempFile(`dcp-${process.pid}-final-bundle-XXXXXXXX`, 'js');\n\n searchPaths = Object.keys(searchPaths);\n webpackInput.writeSync('Object.assign(exports, {\\n');\n for (let moduleId in bundlePathMap) {\n let diskPath = fixSlashes(outerRequire.resolve(bundlePathMap[moduleId]));\n webpackInput.writeSync(` \"${moduleId}\": require(\"${diskPath}\"),\\n`);\n }\n \n webpackInput.writeSync('});\\n');\n webpackInput.writeSync(`defineSearchPaths(${JSON.stringify(searchPaths)});\\n`);\n webpackInput.writeSync(`defineResolveMap(${JSON.stringify(resolveMap)});\\n`);\n /* Run webpack in a separate process to work around issues with require()ing it\n * from a local git repo via npm which is not this git repo. The output of the\n * webpack is the module bundle, in amd define() transport format.\n */\n configuration = {\n mode: buildMode === 'debug' ? 'development' : 'release',\n optimization: { minimize: buildMode === 'debug' },\n entry: webpackInput.filename,\n output: {\n path: path.dirname(webpackOutput.filename),\n filename: path.basename(webpackOutput.filename),\n libraryTarget: 'amd',\n library: 'dcp-localhost',\n },\n }\n\n switch(parseInt(webpack.version, 10))\n {\n case 0: case 1: case 2: case 3:\n throw new Error('Webpack ' + webpack.version + ' is not supported');\n case 4:\n {\n console.warn('Warning: webpack 4 is deprecated - please upgrade!');\n // configuration.output.ecmaVersion = 6;\n configuration.output.pathinfo = buildMode === 'debug';\n\n // Remove fs module from generated bundle\n configuration.node = { fs: 'empty' };\n\n if (!process.env.DCP_DEBUG_WEBPACK)\n {\n args.push('--log-level=warn');\n }\n break;\n }\n default:\n console.warn('Warning: webpack ' + webpack.version + ' not officially supported');\n /* fallthrough */\n case 5:\n {\n configuration.output.pathinfo = buildMode === 'debug';\n configuration.target = [ 'es6' ];\n\n // Need to set a default script chunk format based on the target.\n configuration.output.chunkFormat = 'commonjs';\n\n configuration.resolve = {\n fallback: {\n // Remove fs module from generated bundle\n fs: false,\n }\n };\n break;\n }\n }\n\n args[0] = process.execPath;\n args[1] = fixSlashes(path.resolve(path.dirname(requireNative.resolve('webpack')), '..', 'bin', 'webpack.js'));\n\n webpackConfig.writeSync('module.exports=' + JSON.stringify(configuration));\n args.push(`--config=${webpackConfig.filename}`);\n\n if (dcpConfig.job && dcpConfig.job.modules && dcpConfig.job.modules.webpackOptions)\n args = args.concat(utils.tokenize(dcpConfig.job.modules.webpackOptions));\n if (process.env.DCP_NODE_MODULES_WEBPACK_OPTIONS)\n args = args.concat(utils.tokenize(process.env.DCP_NODE_MODULES_WEBPACK_OPTIONS));\n\n debugging() && console.log('Creating job.requires(dependencies) bundle:', args);\n child = child_process.spawnSync(args[0], args.slice(1), {\n shell: false,\n windowsHide: true,\n stdio: 'inherit',\n });\n\n if (child.status !== 0)\n throw new Error(`Child process returned exit code ${child.status}`);\n \n /* Add a wrapper function around the webpack output which turns it into a valid\n * CommonJS Modules/2.0-draft8 (eg BravoJS) module that does not pollute scope.\n * Calculate the while we're at it to avoid double-reading large files.\n */\n ({prologue, epilogue} = sideloadGenerator());\n md5Stream = __webpack_require__(/*! create-hash */ \"./node_modules/create-hash/browser.js\")('md5');\n finalOutput.writeSync(prologue);\n md5Stream.write(prologue);\n utils.catFile(webpackOutput, finalOutput, md5Stream);\n finalOutput.writeSync(epilogue);\n md5Stream.write(epilogue);\n md5Stream.end();\n } finally {\n webpackInput.closeSync();\n webpackConfig.closeSync();\n finalOutput.closeSync();\n\n webpackInput.remove();\n webpackConfig.remove();\n webpackOutput.remove();\n }\n\n return {\n tempFile: finalOutput,\n hash: md5Stream.read(),\n unresolved: unresolved,\n }\n}\n\n/** \n * Inject the modules in a module bundle into the current BravoJS environment's\n * internal module memo, after invoking module.declare() so that we can masquerade \n * as a simple module dependency.\n *\n * @note This function exists in source form so that it isn't annoying to edit. \n * It gets used by being converted to string and parsed by sideloadGenerator().\n * The line containing the define(\"webpack...\") string must not be changed, \n * as it is a parse token.\n *\n * Additionally, all newlines will be eliminated to preserve the webpack source\n * map, so no C++-style comments should be used, and semicolons are mandatory.\n * Whitespace is also compacted, even inside string literals.\n */\nconst sideloader = `function sideloader() {\n module.declare(function(){});\n (function IIFE() { /* inject webpacked module group into BravoJS' module memo */\n /* Add program module paths discovered during bundling to global require path */\n function defineSearchPaths(searchPaths) {\n for (path of searchPaths)\n bravojs.paths.push(path);\n }\n /* Add name mapping for requires args to cover cases where resolution happens in package.json */\n function defineResolveMap(pathMap) {\n if (!bravojs.resolveMap)\n bravojs.resolveMap = {};\n bravojs.resolveMap = Object.assign(bravojs.resolveMap, pathMap);\n if (!bravojs.globalResolveHook) {\n bravojs.globalResolveHook = function dcp$$define$resolve(moduleIdentifier) {\n if (bravojs.resolveMap.hasOwnProperty(moduleIdentifier))\n return bravojs.resolveMap[moduleIdentifier];\n else\n return moduleIdentifier;\n }\n }\n }\n function define(amdModuleLabel, amdModuleDeps, amdModuleFactory) {\n var moduleGroup = amdModuleFactory();\n\n for (let moduleIdentifier in moduleGroup) {\n if (!moduleGroup.hasOwnProperty(moduleIdentifier))\n continue;\n bravojs.requireMemo[moduleIdentifier] = moduleGroup[moduleIdentifier];\n if (bravojs.debug)\n console.log(\\`Memoized module \\${moduleIdentifier}: \\${Object.keys(moduleGroup[moduleIdentifier])}\\`);\n }\n if (bravojs.mainModule !== '/localhost/program') {\n bravojs.mainModuleDir = '/localhost/program';\n bravojs.global.require = bravojs.requireFactory(bravojs.mainModuleDir);\n }\n }\n define(\"webpack amd output here\", [], factoryFunction);\n })();\n}`;\n\n/** Generate the prologue and epilogue for a webpack in amd library mode which, when \n * all three parts are joined, will sideload the webpack's exports into BravoJS'\n * running require memo, exposing them as require() modules.\n */\nfunction sideloadGenerator() {\n var slInner = sideloader.toString().replace(/^[^{]*{/,'').replace(/} *$/,'');\n var offset_define = slInner.indexOf('define(\"webpack amd output here\"');\n var offset_epilogue = slInner.slice(offset_define).indexOf('\\n');\n var prologue, epilogue;\n \n if (offset_define === -1 || offset_epilogue === -1) {\n throw new Error(`sideload function missing parse token(s)`);\n }\n\n prologue = slInner.slice(0, offset_define) .replace(/[ \\n\\r\\t]+/g, ' ').replace(/(^ | $)/g,'');\n epilogue = slInner.slice(offset_define + offset_epilogue).replace(/[ \\n\\r\\t]+/g, ' ').replace(/(^ | $)/g,'');\n\n return {prologue, epilogue};\n}\n\n/** Return the canonical module id for a given module. All of the modules begin\n * begin in the localhost directory off the conceptual module system root, as these\n * are \"local\" -- not deployed -- modules. Otherwise, they mostly represent the\n * structure on disk, although this is explicitly a path within the conceptual module\n * system space and not actually any physical filesystem.\n * \n * In \"release\" mode, we further modify the canonical path to preserve privacy\n * wrt the developer's disk layout: All search-path directories are \"collapsed\" \n * onto a single directory, and the program-module directory is anonymized. In\n * that case, the program module's \"directory\" is /localhost/program/ and the\n * search path is /localhost/search/.\n *\n * @param {string} moduleIdentifier a module identifier (argument to require()) or absolute path to module\n * @param {function} outerRequire the require() function used by the node code (not the work function)\n * @param {string} mainModuleDir the directory of the main (program) module; used to calculate safety\n * of relative paths when in path-privacy mode. Passing excatly null\n * disables this path-privacy mode.\n * @param {Array} modulePaths List of paths to search (like require.paths in CJS)\n */\nfunction canonicalize(moduleIdentifier, outerRequire, mainModuleDir, modulePaths) {\n var filename, fno;\n var canonical = '';\n var doPathPrivacy = pathPrivacyMode && mainModuleDir !== null;\n \n try {\n filename = (path.isAbsolute(moduleIdentifier) ? path : outerRequire).resolve(moduleIdentifier);\n fixSlashes(filename);\n } catch(e) {\n if (moduleIdentifier.startsWith('./') ||\n moduleIdentifier.startsWith('../') ||\n moduleIdentifier.startsWith('/') ||\n path.isAbsolute(moduleIdentifier)) {\n throw new Error(`Cannot locate module '${moduleIdentifier}'`);\n }\n debugging() && console.warn(`Cannot locate module ${moduleIdentifier}; assuming on deployed search path`);\n return null;\n }\n\n fno = path.parse(filename);\n if (os.platform() === 'win32' && fno.root.match(/[A-Za-z]:/)) {\n /* put drive letters into a virtual mount point */\n let newDir = '/mnt/' + fno.root[0].toLowerCase();\n if (fno.dir != fno.root)\n newDir += '/' + fno.dir.slice(fno.root.length);\n fno.root = '/';\n fno.dir = newDir;\n }\n /* root is always /\n * dir always starts with a / unless its empty and does not end in a /\n * base never starts with a / and does not end in a /\n */ \n canonical = fno.root;\n if (fno.dir)\n canonical += fno.dir.slice(fno.root.length);\n if (fno.dir && fno.dir.length !== fno.root.length) /* if the directory is root we don't need an extra '/' */\n canonical += '/'\n canonical += fno.name;\n\n if (doPathPrivacy) {\n let mmRE = new RegExp(`^${mainModuleDir}(/|$)`);\n if (canonical.match(mmRE)) {\n canonical = '/program' + canonical.slice(mainModuleDir.length);\n } else {\n if (moduleIdentifier.startsWith('./') || moduleIdentifier.startsWith('../')) {\n throw new Error(`Relative module identifiers above the program module's directory are not supported in path privacy mode (${canonical})`);\n }\n canonical = '/search' + moduleIdentifier;\n }\n }\n\n if (doPathPrivacy)\n return '/localhost' + canonical;\n else\n return canonical;\n}\n\n\n//# sourceURL=webpack://dcp/./src/dcp-client/job/node-modules.js?");
4183
4232
 
4184
4233
  /***/ }),
4185
4234
 
@@ -4189,7 +4238,7 @@ eval("/**\n * @file node-modules.js Node-specific support for sen
4189
4238
  \*********************************************/
4190
4239
  /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
4191
4240
 
4192
- eval("/**\n * @file job/result-handle.js\n * @author Ryan Rossiter, ryan@kingsds.network\n * @date June 2020\n *\n * The ResultHandle acts as a proxy for a job's results, querying\n * internal results when available or the scheduler when the results\n * are not available locally.\n */\n\nconst { rehydrateRange } = __webpack_require__(/*! ../range-object */ \"./src/dcp-client/range-object.js\");\nconst protocolV4 = __webpack_require__(/*! dcp/protocol-v4 */ \"./src/protocol-v4/index.js\");\nconst wallet = __webpack_require__(/*! dcp/dcp-client/wallet */ \"./src/dcp-client/wallet/index.js\");\nconst { fetchURI } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\nconst customInspectSymbol = Symbol.for('nodejs.util.inspect.custom');\n\n// Array methods that don't modify the array, will be applied to the ResultHandle's values object\nconst RH_ARRAY_METHODS = [\n 'slice', 'filter', 'concat', 'find', 'findIndex', 'indexOf', 'map', 'reduce', 'includes', 'toString', 'forEach'\n];\n\n/**\n * This class represents a handle on a job's results. It can be used for accessing the job's results, or for querying the scheduler to fetch results.\n * In addition to the properties and methods of this class, the following standard array methods are also available for accessing the available results: `slice`, `filter`, `concat`, `find`, `findIndex`, `indexOf`, `map`, `reduce`, `includes`, `toString`, and `forEach`.\n * The results can also be accessed by index (`results[i]`), but an error will be thrown if the result for that index is not yet available.\n * @access public\n */\nclass ResultHandle extends Array\n{\n constructor(job)\n {\n super();\n\n const { Job } = __webpack_require__(/*! . */ \"./src/dcp-client/job/index.js\");\n if (!(job instanceof Job))\n throw new TypeError('ResultHandle must be constructed from a Job object');\n\n /**\n * The length of the available results array.\n * @type {number}\n * @access public\n */\n this.length = 0; // overridden by the proxy, here for JSDoc purposes\n\n this.job = job;\n this.realKeys = job.jobInputData;\n this.realValues = [];\n this.valuesAvailable = [];\n\n return new Proxy(this, {\n get: (target, name) => {\n if ((typeof name === 'string' || typeof name === 'number') && Number.isInteger(parseFloat(name)))\n {\n let i = parseFloat(name);\n if (target.isAvailable(i))\n return target.realValues[i];\n else\n throw new Error(`Result ${i} is not available. It has either not been computed or you need to fetch it.`);\n } \n else if (name === 'length')\n return target.getLength();\n else if (name === 'constructor')\n return Array.constructor;\n else if (RH_ARRAY_METHODS.includes(name))\n {\n let values = target.values();\n return values[name].bind(values);\n }\n else\n {\n // only return methods on this class, don't allow access\n // to array methods other than the whitelisted ones\n let p = target.__proto__[name];\n if (typeof p === 'function') {\n return p.bind(target);\n } else {\n return p;\n }\n }\n }\n });\n }\n\n toJSON ()\n {\n return JSON.stringify(this.values());\n }\n\n isAvailable(index)\n {\n return this.valuesAvailable[index];\n }\n\n reset() {\n // quickly empty the realValues array\n this.realValues.length = this.valuesAvailable.length = 0;\n this.realValues.length = this.valuesAvailable.length =this.realKeys.length;\n }\n\n /**\n * Returns an array of input values. Will only return input values that have a completed result available.\n * @access public\n * @returns {Array<*>}\n */\n keys() {\n // Keys can be a RangeObject, faster to iterate over valuesAvailable\n return this.valuesAvailable.reduce((keysList, valueAvailable, sliceNumber) => {\n if (valueAvailable) keysList.push(this.realKeys[sliceNumber - 1]);\n return keysList;\n }, []);\n }\n\n /**\n * Returns an array of results. Will only return results that have been received from the scheduler, if only one result is complete the array will contain one value.\n * @access public\n * @returns {Array<*>}\n */\n values() {\n return this.realValues.filter((v, i) => this.isAvailable(i));\n }\n\n // Aliased as length in proxy, can't use getter because Array.length is not configurable\n getLength() {\n return this.valuesAvailable.reduce((n, v) => (n + (v ? 1 : 0)), 0);\n }\n \n [customInspectSymbol]() /* overrides the output when console.log(result) is called */\n { \n return this.values(); \n }\n\n [Symbol.iterator]() {\n let index = 0;\n let values = this.values(); // use available values\n\n return {\n next: () => ({\n value: values[index++],\n done: index > values.length\n })\n };\n }\n\n /** \n * Returns an array of [input, output] pairs, in sliceNumber order.\n * Return value is undefined if the input is not an ES5 primitive.\n * @access public\n * @returns {Array<*>}\n */\n entries() {\n return this.valuesAvailable.reduce((keyValuePairs, valueAvailable, sliceNumber) => {\n if (valueAvailable) keyValuePairs.push([\n String(this.realKeys[sliceNumber - 1]),\n this.realValues[sliceNumber],\n ]);\n return keyValuePairs;\n }, []);\n }\n\n /** \n * Returns an Object associating input and output values where the inputs are ES5 primitive types.\n * Return value is undefined if the input is not an ES5 primitive.\n * @access public\n * @returns {object}\n */\n fromEntries() {\n return this.entries().reduce((o, [k, v]) => {\n o[k] = v; return o;\n }, {});\n }\n\n /** \n * Return the nth input value/input vector \n * @access public\n * @param {number} n Index in the input set to return the value for.\n * @returns {*} Input set value\n */\n key(n) {\n return this.realKeys[n];\n }\n\n /** \n * Return the value corresponding to the provided key \n * @access public\n * @param {*} key Corresponds to a value in the job's input set.\n * @returns {*} Result corresponding to the input value\n */\n lookupValue(key) {\n // use keys instead of _keys so we only lookup on available results\n let ind = this.keys().indexOf(key);\n if (ind === -1)\n throw new DCPError('Argument passed into the function was not found in input set', 'ERANGE');\n\n return this.values()[ind];\n }\n\n /**\n * Sends request to scheduler to fetch results, the retrieved results will be populated on this object.\n * @param {RangeObject} [rangeObject] - range object to query results\n * @param {string} [emitEvents] - if true, emits a `result` event for new results as they are added to the handle, or emits for all results if set to 'all'\n * @access public\n * @emits Job#resultsUpdated\n */\n async fetch(rangeObject, emitEvents) {\n const range = rangeObject && rehydrateRange(rangeObject);\n\n const job = this.job;\n \n // Fetch any existing results\n let ks = await wallet.getId();\n const conn = new protocolV4.Connection(dcpConfig.scheduler.services.resultSubmitter.location, ks);\n\n const { success, payload } = await conn.send('fetchResult', {\n job: job.id,\n owner: job.paymentAccountKeystore.address,\n range,\n }, job.paymentAccountKeystore);\n\n // Unpack results, using fetchURI to decode/fetch result URIs\n await Promise.all(payload.map(async r => {\n if (!r) {\n console.warn(`ResultHandle.fetch: Received result was not defined (${r}), ignoring...`);\n return;\n }\n\n this.realValues[r.slice] = await fetchURI(decodeURIComponent(r.value), dcpConfig.scheduler.location.origin);\n\n if (emitEvents && (emitEvents === 'all' || !this.valuesAvailable[r.slice])) {\n job.emit('result', { sliceNumber: r.slice, result: this.realValues[r.slice] });\n }\n this.valuesAvailable[r.slice] = true;\n }));\n\n job.emit('resultsUpdated');\n await conn.close(null, true);\n }\n \n /**\n * Handle adding a new result\n */\n newResult(result, index)\n {\n if (this.valuesAvailable[index])\n throw new Error(`ResultHandle: result ${index} already exists, cannot add it as new result.`)\n this.realValues[index] = result;\n this.valuesAvailable[index] = true;\n }\n\n async list(rangeObject) {\n const range = rehydrateRange(rangeObject);\n throw new Error(\"ResultHandle.list not implemented\");\n }\n\n async delete(rangeObject) {\n const range = rehydrateRange(rangeObject);\n throw new Error(\"ResultHandle.delete not implemented\");\n }\n\n async stat(rangeObject) {\n const range = rehydrateRange(rangeObject);\n throw new Error(\"ResultHandle.stat not implemented\");\n }\n}\n\nObject.assign(exports, {\n ResultHandle,\n});\n\n\n//# sourceURL=webpack://dcp/./src/dcp-client/job/result-handle.js?");
4241
+ eval("/**\n * @file job/result-handle.js\n * @author Ryan Rossiter, ryan@kingsds.network\n * Robert Mirandola, <robert@kingsds.network>\n * @date June 2020\n * July 2022\n *\n * The ResultHandle acts as a proxy for a job's results, querying\n * internal results when available or the scheduler when the results\n * are not available locally.\n */\n\nconst { rehydrateRange, RangeObject, SparseRangeObject } = __webpack_require__(/*! ../range-object */ \"./src/dcp-client/range-object.js\");\nconst protocolV4 = __webpack_require__(/*! dcp/protocol-v4 */ \"./src/protocol-v4/index.js\");\nconst wallet = __webpack_require__(/*! dcp/dcp-client/wallet */ \"./src/dcp-client/wallet/index.js\");\nconst { fetchURI } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\nconst customInspectSymbol = Symbol.for('nodejs.util.inspect.custom');\n\n// Array methods that don't modify the array, will be applied to the ResultHandle's values object\nconst RH_ARRAY_METHODS = [\n 'slice', 'filter', 'concat', 'find', 'findIndex', 'indexOf', 'map', 'reduce', 'includes', 'toString', 'forEach'\n];\n\n/**\n * This class represents a handle on a job's results. It can be used for accessing the job's results, or for querying the scheduler to fetch results.\n * In addition to the properties and methods of this class, the following standard array methods are also available for accessing the available results: `slice`, `filter`, `concat`, `find`, `findIndex`, `indexOf`, `map`, `reduce`, `includes`, `toString`, and `forEach`.\n * The results can also be accessed by index (`results[i]`), but an error will be thrown if the result for that index is not yet available.\n * @access public\n */\nclass ResultHandle extends Array\n{\n constructor(job)\n {\n super();\n\n const { Job } = __webpack_require__(/*! . */ \"./src/dcp-client/job/index.js\");\n if (!(job instanceof Job))\n throw new TypeError('ResultHandle must be constructed from a Job object');\n\n /**\n * The length of the available results array.\n * @type {number}\n * @access public\n */\n this.length = 0; // overridden by the proxy, here for JSDoc purposes\n\n this.job = job;\n this.realKeys = job.jobInputData;\n this.realValues = [];\n this.valuesAvailable = [];\n\n return new Proxy(this, {\n get: (target, name) => {\n if ((typeof name === 'string' || typeof name === 'number') && Number.isInteger(parseFloat(name)))\n {\n let i = parseFloat(name);\n if (target.isAvailable(i))\n return target.realValues[i];\n else\n throw new Error(`Result ${i} is not available. It has either not been computed or you need to fetch it.`);\n } \n else if (name === 'length')\n return target.getLength();\n else if (name === 'constructor')\n return Array.constructor;\n else if (RH_ARRAY_METHODS.includes(name))\n {\n let values = target.values();\n return values[name].bind(values);\n }\n else\n {\n // only return methods on this class, don't allow access\n // to array methods other than the whitelisted ones\n let p = target.__proto__[name];\n if (typeof p === 'function') {\n return p.bind(target);\n } else {\n return p;\n }\n }\n }\n });\n }\n\n toJSON ()\n {\n return JSON.stringify(this.values());\n }\n\n isAvailable(index)\n {\n return this.valuesAvailable[index];\n }\n\n reset() {\n // quickly empty the realValues array\n this.realValues.length = this.valuesAvailable.length = 0;\n this.realValues.length = this.valuesAvailable.length =this.realKeys.length;\n }\n\n /**\n * Returns an array of input values. Will only return input values that have a completed result available.\n * @access public\n * @returns {Array<*>}\n */\n keys() {\n // Keys can be a RangeObject, faster to iterate over valuesAvailable\n return this.valuesAvailable.reduce((keysList, valueAvailable, sliceNumber) => {\n if (valueAvailable) keysList.push(this.realKeys[sliceNumber - 1]);\n return keysList;\n }, []);\n }\n\n /**\n * Returns an array of results. Will only return results that have been received from the scheduler, if only one result is complete the array will contain one value.\n * @access public\n * @returns {Array<*>}\n */\n values() {\n return this.realValues.filter((v, i) => this.isAvailable(i));\n }\n\n // Aliased as length in proxy, can't use getter because Array.length is not configurable\n getLength() {\n return this.valuesAvailable.reduce((n, v) => (n + (v ? 1 : 0)), 0);\n }\n \n [customInspectSymbol]() /* overrides the output when console.log(result) is called */\n { \n return this.values(); \n }\n\n [Symbol.iterator]() {\n let index = 0;\n let values = this.values(); // use available values\n\n return {\n next: () => ({\n value: values[index++],\n done: index > values.length\n })\n };\n }\n\n /** \n * Returns an array of [input, output] pairs, in sliceNumber order.\n * Return value is undefined if the input is not an ES5 primitive.\n * @access public\n * @returns {Array<*>}\n */\n entries() {\n return this.valuesAvailable.reduce((keyValuePairs, valueAvailable, sliceNumber) => {\n if (valueAvailable) keyValuePairs.push([\n String(this.realKeys[sliceNumber - 1]),\n this.realValues[sliceNumber],\n ]);\n return keyValuePairs;\n }, []);\n }\n\n /** \n * Returns an Object associating input and output values where the inputs are ES5 primitive types.\n * Return value is undefined if the input is not an ES5 primitive.\n * @access public\n * @returns {object}\n */\n fromEntries() {\n return this.entries().reduce((o, [k, v]) => {\n o[k] = v; return o;\n }, {});\n }\n\n /** \n * Return the nth input value/input vector \n * @access public\n * @param {number} n Index in the input set to return the value for.\n * @returns {*} Input set value\n */\n key(n) {\n return this.realKeys[n];\n }\n\n /** \n * Return the value corresponding to the provided key \n * @access public\n * @param {*} key Corresponds to a value in the job's input set.\n * @returns {*} Result corresponding to the input value\n */\n lookupValue(key) {\n // use keys instead of _keys so we only lookup on available results\n let ind = this.keys().indexOf(key);\n if (ind === -1)\n throw new DCPError('Argument passed into the function was not found in input set', 'ERANGE');\n\n return this.values()[ind];\n }\n\n /**\n * Sends request to scheduler to fetch results, the retrieved results will be populated on this object.\n * @param {RangeObject} [rangeObject] - range object to query results\n * @param {string} [emitEvents] - if truthy, emits a `result` event for new results as they are added to the handle, and if set to 'all' emits 'resultUpdated' for duplicate results \n * @access public\n * @emits Job#resultsUpdated\n */\n async fetch(rangeObject, emitEvents) {\n const range = rangeObject && rehydrateRange(rangeObject);\n\n const job = this.job;\n \n // Fetch any existing results\n let ks = await wallet.getId();\n const conn = new protocolV4.Connection(dcpConfig.scheduler.services.resultSubmitter.location, ks);\n\n const { success, payload } = await conn.send('fetchResult', {\n job: job.id,\n owner: job.paymentAccountKeystore.address,\n range,\n }, job.paymentAccountKeystore);\n\n // Unpack results, using fetchURI to decode/fetch result URIs\n await Promise.all(payload.map(async r => {\n if (!r) {\n console.warn(`ResultHandle.fetch: Received result was not defined (${r}), ignoring...`);\n return;\n }\n\n const decodedValue = decodeURIComponent(r.value)\n try\n {\n this.realValues[r.slice] = await fetchURI(decodedValue, dcpConfig.scheduler.location.origin); \n } catch (error)\n {\n // ensure the error is an actual URI parsing error and not due actual problems\n if (error.code === 'ERR_INVALID_URL')\n this.realValues[r.slice] = decodedValue;\n else\n return console.warn('Error in ResultHandle.fetch():', error.message);\n }\n\n if (emitEvents)\n {\n if (!this.valuesAvailable[r.slice])\n this.job.emit('result', { sliceNumber: r.slice, result: this.realValues[r.slice] });\n else if (emitEvents === 'all')\n {\n this.job.emit('resultsUpdated', { sliceNumber: r.slice }); \n this.job.emit('result', { sliceNumber: r.slice, result: this.realValues[r.slice] });\n }\n }\n this.valuesAvailable[r.slice] = true;\n }));\n await conn.close(null, true);\n }\n \n /**\n * Handle adding a new result\n */\n newResult(result, index)\n {\n if (this.valuesAvailable[index])\n {\n this.realValues[index] = result;\n this.job.emit('resultsUpdated', { sliceNumber: index }); \n }\n else\n {\n this.valuesAvailable[index] = true;\n this.realValues[index] = result;\n this.job.emit('result', { job: this.job.address, sliceNumber: index, result });\n }\n }\n \n list(rangeObject) {\n let range = rangeObject && rehydrateRange(rangeObject);\n if (!range) range = new RangeObject(0, this.valuesAvailable.length);\n let sparseObjects = [];\n let min = range.start + range.step;\n let goodRange = false;\n let step = range.step;\n for (let i = range.start; i <= range.end; i+= step)\n {\n if (!goodRange && this.isAvailable(i))\n {\n min = i\n goodRange = true;\n }\n if (goodRange && !this.isAvailable(i))\n {\n if (min !== i-step)\n sparseObjects.push(new RangeObject(min, i-step, step))\n else\n sparseObjects.push(new RangeObject(min, i-step))\n goodRange = false;\n }\n }\n if (goodRange)\n {\n if (min !== range.end)\n sparseObjects.push(new RangeObject(min, range.end, step))\n else\n sparseObjects.push(new RangeObject(min, range.end))\n }\n\n try\n {\n return new SparseRangeObject(...sparseObjects);\n }\n catch\n {\n return [];\n }\n\n }\n\n async delete(rangeObject) {\n var range;\n try\n {\n range = rehydrateRange(rangeObject);\n }\n catch\n {\n range = 'all';\n }\n \n const job = this.job;\n let ks = await wallet.getId();\n const conn = new protocolV4.Connection(dcpConfig.scheduler.services.resultSubmitter.location, ks);\n\n const { success, payload } = await conn.send('deleteResult', {\n job: job.id,\n owner: job.paymentAccountKeystore.address,\n range,\n }, job.paymentAccountKeystore);\n \n if (!success)\n throw new Error('ResultHandle.delete: Could not delete slices from scheduler');\n \n await Promise.all(payload.map(r => {\n if (!r) {\n console.warn(`ResultHandle.delete: Received result was not defined (${r}), ignoring...`);\n return;\n }\n this.valuesAvailable[r] = false;\n delete this.realValues[r];\n }));\n \n await conn.close(null, true);\n return payload;\n }\n\n async stat(rangeObject) {\n const range = rehydrateRange(rangeObject);\n throw new Error(\"ResultHandle.stat not implemented\");\n }\n}\n\nObject.assign(exports, {\n ResultHandle,\n});\n\n\n//# sourceURL=webpack://dcp/./src/dcp-client/job/result-handle.js?");
4193
4242
 
4194
4243
  /***/ }),
4195
4244
 
@@ -4209,7 +4258,7 @@ eval("/**\n * @file job/slice-payment-offer.js\n * @author Ryan Ross
4209
4258
  \*********************************************/
4210
4259
  /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
4211
4260
 
4212
- eval("/**\n * @file job/upload-slices.js\n * @author Ryan Saweczko, ryansaweczko@kingsds.network\n * Danny Akbarzadeh, danny@kingsds.network\n * \n * @date Jun 2022\n *\n * Implement functions to upload slices to the scheduler after a job has been deployed.\n * This area will have it's own connection to the job submit service which it is responsible\n * for handling.\n */\n\nconst { Connection } = __webpack_require__(/*! dcp/protocol-v4 */ \"./src/protocol-v4/index.js\");\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\nconst kvin = __webpack_require__(/*! kvin */ \"./node_modules/kvin/kvin.js\");\n\nconst debugging = (__webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope)('dcp-client');\n\nlet uploadConnection = null;\nlet uploadRefs = 0;\nlet tunedKvin;\n\nfunction createNewConnection()\n{\n uploadConnection = new Connection(dcpConfig.scheduler.services.jobSubmit);\n debugging('slice-upload') && console.debug('025: opening new uploadConnection', uploadConnection.id);\n uploadConnection.on('close', () => {\n debugging('slice-upload') && console.debug('027: uploadConnection is closing', uploadConnection?.id);\n uploadConnection = null;\n });\n return uploadConnection.connect();\n}\n\n/*\nHelper function: reprepare a message if a connection breaks before error tolerance\n*/\nfunction reprepareMessage(details)\n{\n return uploadConnection.prepare('addSliceData', {\n job: details.job,\n dataValues: kvinMarshal(details.pile),\n });\n}\n\n/**\n * Helper function that tries to upload slicePile to scheduler for the job with the given address\n * If the connection throws, we will continue trying to upload until it has thrown errorTolerance times\n * However, if the upload is unsuccessful, we throw immediately.\n * @param {object} pileMessage \n * @param {object} originalMessageDetails - used if the connection breaks and we need to reprepare the message.\n * @returns payload containing success property (pertaining to success of adding slices to job) as well as lastSliceNumber of job \n */\nasync function safeSliceUpload(pileMessage, originalMessageDetails)\n{\n debugging('slice-upload') && console.debug('055: safeSliceUpload()', {\n slices: originalMessageDetails.pile.length,\n job: originalMessageDetails.job,\n });\n let payload = undefined;\n let errorTolerance = dcpConfig.job.sliceUploadErrorTolerance; // copy number of times we will tolerate non-success when uploading slices directly from config\n\n while (true) // eslint-disable-line no-constant-condition\n {\n try\n {\n const start = Date.now();\n debugging('slice-upload') && console.log('x-dbg-uploadStart', {length:pileMessage.signedMessage.length});\n // If the connection closes after we prepare the message but before our error tolerance is reached, re-prepare and continue trying to send message.\n if (!uploadConnection)\n {\n debugging('slice-upload') && console.log('x-dbg-openNewConnection');\n await createNewConnection();\n pileMessage = await reprepareMessage(originalMessageDetails); /* Assuming preparing a identical message will result in the same length */\n }\n\n debugging('slice-upload') && console.debug('070: sending addSliceData request...');\n payload = await uploadConnection.sendPreparedMessage(pileMessage);\n debugging('slice-upload') && console.debug('072: success=', { success: payload?.success, payload: payload?.payload || payload });\n if (!payload.success)\n {\n debugging('slice-upload') && console.log('x-dbg-uploadBackoff', {length:pileMessage.signedMessage.length});\n throw payload.payload;\n }\n else\n {\n debugging('slice-upload') && console.log('x-dbg-uploadProgress', { el: Date.now() - start});\n break;\n }\n }\n catch (error)\n {\n debugging('slice-upload') && console.debug('080: caught upload error:', error?.code, error?.message || error, `${errorTolerance-1} tries left...`);\n if (uploadConnection) {\n debugging('slice-upload') && console.debug(`089: closing connection`, uploadConnection.id);\n await uploadConnection.close(); // clean up the connection on upload error\n debugging('slice-upload') && console.debug(`091: closed`, uploadConnection?.id);\n }\n\n if (--errorTolerance <= 0)\n {\n debugging('slice-upload') && console.log('x-dbg-uploadError');\n throw error;\n }\n \n if (error.slicesSaved) {\n const newPile = originalMessageDetails.pile.slice(error.slicesSaved);\n\n debugging('slice-upload') && console.debug(`103: recursing into addSlices with a new pile of ${newPile.length}...`)\n // recursively call back into addSlices, with only the unsaved remainder of this pile\n const retry = await addSlices(newPile, originalMessageDetails.job);\n\n // if that worked, then break out of this loop\n if (retry.success) {\n payload = {\n success: retry.success,\n payload: retry,\n };\n break;\n }\n }\n }\n }\n debugging('slice-upload') && console.debug(`120< safeSliceUpload() is returning`, {\n success: payload.success,\n payload: payload.payload,\n });\n return payload;\n}\n\n/**\n* This function contains the actual logic behind staggered slice uploads\n* to the scheduler which makes quicker deployment possible.\n* \n* Note that we pass in mostToTake so that the uploadLogic function can update \n* it to the new value it needs to be, and then pass it back to the wrapper \n* function (addSlices) which actually does the work of picking up slices \n* and thus uses this value\n* @param {Array} pile the actual array of slices being uploaded to scheduler\n* @param {Number} mostToTake number of slices that should be taken by the wrapper function (addSlices) \n* which actually does the work of picking up slices and thus uses this value.\n* We pass in mostToTake so that the uploadLogic function can update it to the \n* new value it needs to be, and then pass it back to the wrapper\n* @param {*} jobAddress Address of job to upload the slices to \n* @returns payload containing success property (pertaining to success of adding slices to job) as well as lastSliceNumber of job\n*/\nasync function sliceUploadLogic(pile, mostToTake, jobAddress)\n{\n debugging('slice-upload') && console.debug(`111: sliceUploadLogic(), ${pile.length} slices, up to ${mostToTake}`);\n if (!uploadConnection)\n await createNewConnection();\n\n const slicesTaken = pile.length;\n let pileMessage = await uploadConnection.prepare('addSliceData', {\n job: jobAddress,\n dataValues: kvinMarshal(pile),\n });\n \n let pileSize = pileMessage.signedMessage.length;\n \n let newMostToTake;\n let uploadedSlices;\n\n // if the pile is larger than the ceiling but we only took one slice, there's no smaller pile we can make\n // so we upload it anyway but we don't try taking more next time cause we were over the ceiling (which \n // is a hard limit on upload sizes)\n if ((pileSize > dcpConfig.job.uploadSlicesCeiling) && (slicesTaken === 1))\n {\n uploadedSlices = await safeSliceUpload(pileMessage, { job: jobAddress, pile });\n newMostToTake = 1;\n }\n \n // if the pile is larger than the target but we only took one slice, there's no smaller pile we can make\n // so we upload it anyway and still try taking more\n else if ((pileSize > dcpConfig.job.uploadSlicesTarget) && (slicesTaken === 1))\n {\n uploadedSlices = await safeSliceUpload(pileMessage, { job: jobAddress, pile });\n newMostToTake = mostToTake * dcpConfig.job.uploadIncreaseFactor;\n }\n \n // otherwise, if the pile is smaller than the soft ceiling, send up the pile anyway (since piles are expensive to make) \n // but remember to include incrementFactor times as many slices in the next pile\n else if (pileSize <= dcpConfig.job.uploadSlicesTarget)\n {\n uploadedSlices = await safeSliceUpload(pileMessage, { job: jobAddress, pile });\n newMostToTake = mostToTake * dcpConfig.job.uploadIncreaseFactor;\n }\n \n // if the pile is over the ceiling then we do not upload and begin reassembling our piles from scratch\n else if (pileSize > dcpConfig.job.uploadSlicesCeiling)\n {\n newMostToTake = -1;\n }\n \n // if the pile is over the target (but implicitly under the ceiling), then upload the pile to scheduler but lower mostToTake\n // by a smaller factor than incrementFactor to allow us to begin \"centering\" sizes of piles around the target\n else if (pileSize > dcpConfig.job.uploadSlicesTarget)\n {\n uploadedSlices = await safeSliceUpload(pileMessage, { job: jobAddress, pile });\n newMostToTake = Math.ceil(mostToTake / ((2 / 3) * dcpConfig.job.uploadIncreaseFactor));\n }\n else\n throw new Error('hopefully impossible code in slice upload logic');\n\n let payload = uploadedSlices ? uploadedSlices.payload : undefined;\n debugging('slice-upload') && console.debug(`198< sliceUploadLogic() is returning`, { payload, newMostToTake });\n return { payload, newMostToTake }; // in case the user needs lastSliceNumber's value\n}\n\n/**\n* Uploads slices to the scheduler in a staggered fashion\n* @param {Array} dataValues actual array of slices being uploaded to scheduler\n* @param {*} jobAddress Address of job these slices are for\n* @param {*} newTunedKvin undefined, or new version of kvin tuned for speed or size specifically. Use if defined.\n* @returns payload containing success property (pertaining to success of adding slices to job) as well as lastSliceNumber of job\n*/\nasync function addSlices(dataValues, jobAddress, newTunedKvin)\n{\n debugging('slice-upload') && console.debug(`178: addSlices:${uploadRefs}()`, {\n slices: dataValues.length,\n job: jobAddress,\n })\n if (newTunedKvin)\n tunedKvin = newTunedKvin;\n\n if (!Array.isArray(dataValues))\n throw new TypeError('Only data-by-value jobs may dynamically add slices');\n\n let mostToTake = dcpConfig.job.uploadInitialNumberOfSlices; // maximum number of slices we could take in per pile\n let payload = undefined; // used in return value\n let slicesTaken = 0; // number of slices in the pile already\n let pile = [];\n uploadRefs++;\n\n for (let slice of dataValues)\n {\n pile.push(slice);\n slicesTaken++;\n if (slicesTaken === mostToTake)\n {\n let total = await sliceUploadLogic(pile, mostToTake, jobAddress);\n payload = total.payload;\n \n if (total.newMostToTake < 0)\n {\n /* if total.newMostToTake == -1 (only non-positive value returned), then the pile was not successfully\n * uploaded because it was over the ceiling and we need to upload the pile *itself* again, recursively\n */\n payload = await addSlices(pile, jobAddress);\n /* and next time, the number of slices we take is the number from this time *divided* by the incrementFactor\n * since we know invariably that number of slices was under the ceiling AND target\n * if you're curious why that's an invariant, this is because mostToTake only ever *increases* by being multiplied by \n * a factor of incrementFactor within sliceUploadLogic, and this only occurs when the pile being uploaded that time\n * was under the target\n */\n mostToTake = mostToTake / dcpConfig.job.uploadIncreaseFactor;\n }\n else\n {\n /* in all other cases (other than the pile size being over the ceiling) the sliceUploadLogic helper \n * determines the number of slices we should pick up next time, so we just use the value it spits out\n */\n mostToTake = total.newMostToTake;\n }\n \n // reset slicesTaken and pile since at this point we know for sure the pile has been uploaded\n pile = [];\n slicesTaken = 0;\n }\n }\n // upload the pile one last time in case we continued off the last slice with a non-empty pile\n if (pile.length !== 0)\n {\n let finalObj = await sliceUploadLogic(pile, mostToTake, jobAddress);\n payload = finalObj.payload;\n mostToTake = finalObj.newMostToTake;\n \n if (mostToTake < 0)\n {\n // if you need documentation on the next two lines, look inside the if (total.newMostToTake < 0) just above\n payload = await addSlices(pile, jobAddress);\n mostToTake = mostToTake / dcpConfig.job.uploadIncreaseFactor;\n }\n }\n\n // and finally assign whatever mostToTake was at the end of this run of the function to be returned \n // as part of the payload in case addSlices was called recursively\n if (!payload) {\n console.warn(`280: payload was somehow not an Object...`, payload);\n payload = {};\n }\n payload.mostToTake = mostToTake;\n\n // cleanup connection when done with this function and all recursive calls\n if (uploadConnection && uploadRefs === 1) {\n debugging('slice-upload') && console.debug(`250: closing uploadConnection`, uploadConnection?.id);\n await uploadConnection.close();\n debugging('slice-upload') && console.debug(`252: closed uploadConnection`, uploadConnection?.id);\n }\n uploadRefs--;\n\n /* contains the job's lastSliceNumber (the only externally-meaningful value returned from \n * the uploading of slices to the scheduler) in case the calling function needs it \n */\n debugging('slice-upload') && console.log(`294< addSlices:${uploadRefs}() is returning:`, payload);\n return payload;\n}\n\n/**\n * marshal the value using kvin or instance of the kvin (tunedKvin)\n * tunedKvin is defined if job.tuning.kvin is specified.\n *\n * @param {any} value \n * @return {object} A marshaled object\n * \n */\nfunction kvinMarshal (value) {\n if (tunedKvin)\n return tunedKvin.marshal(value);\n\n return kvin.marshal(value);\n}\n\n\nexports.addSlices = addSlices;\n\n\n//# sourceURL=webpack://dcp/./src/dcp-client/job/upload-slices.js?");
4261
+ eval("/**\n * @file job/upload-slices.js\n * @author Ryan Saweczko, ryansaweczko@kingsds.network\n * Danny Akbarzadeh, danny@kingsds.network\n * \n * @date Jun 2022\n *\n * Implement functions to upload slices to the scheduler after a job has been deployed.\n * This area will have it's own connection to the job submit service which it is responsible\n * for handling.\n */\n\nconst { Connection } = __webpack_require__(/*! dcp/protocol-v4 */ \"./src/protocol-v4/index.js\");\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\nconst kvin = __webpack_require__(/*! kvin */ \"./node_modules/kvin/kvin.js\");\n\nconst debugging = (__webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope)('dcp-client');\n\nlet uploadConnection = null;\nlet uploadRefs = 0;\nlet tunedKvin;\n\nfunction createNewConnection()\n{\n uploadConnection = new Connection(dcpConfig.scheduler.services.jobSubmit);\n uploadConnection.on('close', () => { uploadConnection = null; });\n return uploadConnection.connect();\n}\n\n/*\nHelper function: reprepare a message if a connection breaks before error tolerance\n*/\nfunction reprepareMessage(details)\n{\n return uploadConnection.prepare('addSliceData', {\n job: details.job,\n dataValues: kvinMarshal(details.pile),\n });\n}\n\n/**\n * Helper function that tries to upload slicePile to scheduler for the job with the given address\n * If the connection throws, we will continue trying to upload until it has thrown errorTolerance times\n * However, if the upload is unsuccessful, we throw immediately.\n * @param {object} pileMessage \n * @param {object} originalMessageDetails - used if the connection breaks and we need to reprepare the message.\n * @returns payload containing success property (pertaining to success of adding slices to job) as well as lastSliceNumber of job \n */\nasync function safeSliceUpload(pileMessage, originalMessageDetails)\n{\n let payload = undefined;\n let errorTolerance = dcpConfig.job.sliceUploadErrorTolerance; // copy number of times we will tolerate non-success when uploading slices directly from config\n\n while (true) // eslint-disable-line no-constant-condition\n {\n try\n {\n const start = Date.now();\n debugging('slice-upload') && console.log('x-dbg-uploadStart', pileMessage.signedMessage.length);\n // If the connection closes after we prepare the message but before our error tolerance is reached, re-prepare and continue trying to send message.\n if (!uploadConnection)\n {\n debugging('slice-upload') && console.log('x-dbg-openNewConnection');\n await createNewConnection();\n pileMessage = await reprepareMessage(originalMessageDetails); /* Assuming preparing a identical message will result in the same length */\n }\n\n payload = await uploadConnection.sendPreparedMessage(pileMessage);\n if (!payload.success)\n {\n debugging('slice-upload') && console.log('x-dbg-uploadBackoff', {length:pileMessage.signedMessage.length});\n throw payload;\n }\n else\n {\n debugging('slice-upload') && console.log('x-dbg-uploadProgress', Date.now() - start);\n break;\n }\n }\n catch (error)\n {\n if (--errorTolerance <= 0)\n {\n debugging('slice-upload') && console.log('x-dbg-uploadError', error);\n if (uploadConnection)\n uploadConnection.close(); // ensure we clean up the connection\n throw error;\n }\n }\n }\n return payload;\n}\n\n/**\n* This function contains the actual logic behind staggered slice uploads\n* to the scheduler which makes quicker deployment possible.\n* \n* Note that we pass in mostToTake so that the uploadLogic function can update \n* it to the new value it needs to be, and then pass it back to the wrapper \n* function (addSlices) which actually does the work of picking up slices \n* and thus uses this value\n* @param {Array} pile the actual array of slices being uploaded to scheduler\n* @param {Number} mostToTake number of slices that should be taken by the wrapper function (addSlices) \n* which actually does the work of picking up slices and thus uses this value.\n* We pass in mostToTake so that the uploadLogic function can update it to the \n* new value it needs to be, and then pass it back to the wrapper\n* @param {*} jobAddress Address of job to upload the slices to \n* @returns payload containing success property (pertaining to success of adding slices to job) as well as lastSliceNumber of job\n*/\nasync function sliceUploadLogic(pile, mostToTake, jobAddress)\n{\n if (!uploadConnection)\n await createNewConnection();\n\n const slicesTaken = pile.length;\n let pileMessage = await uploadConnection.prepare('addSliceData', {\n job: jobAddress,\n dataValues: kvinMarshal(pile),\n });\n \n let pileSize = pileMessage.signedMessage.length;\n \n let newMostToTake;\n let uploadedSlices;\n\n // if the pile is larger than the ceiling but we only took one slice, there's no smaller pile we can make\n // so we upload it anyway but we don't try taking more next time cause we were over the ceiling (which \n // is a hard limit on upload sizes)\n if ((pileSize > dcpConfig.job.uploadSlicesCeiling) && (slicesTaken === 1))\n {\n uploadedSlices = await safeSliceUpload(pileMessage, { job: jobAddress, pile });\n newMostToTake = 1;\n }\n \n // if the pile is larger than the target but we only took one slice, there's no smaller pile we can make\n // so we upload it anyway and still try taking more\n else if ((pileSize > dcpConfig.job.uploadSlicesTarget) && (slicesTaken === 1))\n {\n uploadedSlices = await safeSliceUpload(pileMessage, { job: jobAddress, pile });\n newMostToTake = mostToTake * dcpConfig.job.uploadIncreaseFactor;\n }\n \n // otherwise, if the pile is smaller than the soft ceiling, send up the pile anyway (since piles are expensive to make) \n // but remember to include incrementFactor times as many slices in the next pile\n else if (pileSize <= dcpConfig.job.uploadSlicesTarget)\n {\n uploadedSlices = await safeSliceUpload(pileMessage, { job: jobAddress, pile });\n newMostToTake = mostToTake * dcpConfig.job.uploadIncreaseFactor;\n }\n \n // if the pile is over the ceiling then we do not upload and begin reassembling our piles from scratch\n else if (pileSize > dcpConfig.job.uploadSlicesCeiling)\n {\n newMostToTake = -1;\n }\n \n // if the pile is over the target (but implicitly under the ceiling), then upload the pile to scheduler but lower mostToTake\n // by a smaller factor than incrementFactor to allow us to begin \"centering\" sizes of piles around the target\n else if (pileSize > dcpConfig.job.uploadSlicesTarget)\n {\n uploadedSlices = await safeSliceUpload(pileMessage, { job: jobAddress, pile });\n newMostToTake = Math.ceil(mostToTake / ((2 / 3) * dcpConfig.job.uploadIncreaseFactor));\n }\n else\n throw new Error('hopefully impossible code in slice upload logic');\n\n let payload = uploadedSlices ? uploadedSlices.payload : undefined;\n return { payload, newMostToTake }; // in case the user needs lastSliceNumber's value\n}\n\n/**\n* Uploads slices to the scheduler in a staggered fashion\n* @param {Array} dataValues actual array of slices being uploaded to scheduler\n* @param {*} jobAddress Address of job these slices are for\n* @param {*} newTunedKvin undefined, or new version of kvin tuned for speed or size specifically. Use if defined.\n* @returns payload containing success property (pertaining to success of adding slices to job) as well as lastSliceNumber of job\n*/\nasync function addSlices(dataValues, jobAddress, newTunedKvin)\n{\n if (newTunedKvin)\n tunedKvin = newTunedKvin;\n\n if (!Array.isArray(dataValues))\n throw new TypeError('Only data-by-value jobs may dynamically add slices');\n\n let mostToTake = dcpConfig.job.uploadInitialNumberOfSlices; // maximum number of slices we could take in per pile\n let payload = undefined; // used in return value\n let slicesTaken = 0; // number of slices in the pile already\n let pile = [];\n uploadRefs++;\n\n for (let slice of dataValues)\n {\n pile.push(slice);\n slicesTaken++;\n if (slicesTaken === mostToTake)\n {\n let total = await sliceUploadLogic(pile, mostToTake, jobAddress);\n payload = total.payload;\n \n if (total.newMostToTake < 0)\n {\n /* if total.newMostToTake == -1 (only non-positive value returned), then the pile was not successfully\n * uploaded because it was over the ceiling and we need to upload the pile *itself* again, recursively\n */\n payload = await addSlices(pile, jobAddress);\n /* and next time, the number of slices we take is the number from this time *divided* by the incrementFactor\n * since we know invariably that number of slices was under the ceiling AND target\n * if you're curious why that's an invariant, this is because mostToTake only ever *increases* by being multiplied by \n * a factor of incrementFactor within sliceUploadLogic, and this only occurs when the pile being uploaded that time\n * was under the target\n */\n mostToTake = mostToTake / dcpConfig.job.uploadIncreaseFactor;\n }\n else\n {\n /* in all other cases (other than the pile size being over the ceiling) the sliceUploadLogic helper \n * determines the number of slices we should pick up next time, so we just use the value it spits out\n */\n mostToTake = total.newMostToTake;\n }\n \n // reset slicesTaken and pile since at this point we know for sure the pile has been uploaded\n pile = [];\n slicesTaken = 0;\n }\n }\n // upload the pile one last time in case we continued off the last slice with a non-empty pile\n if (pile.length !== 0)\n {\n let finalObj = await sliceUploadLogic(pile, mostToTake, jobAddress);\n payload = finalObj.payload;\n mostToTake = finalObj.newMostToTake;\n \n if (mostToTake < 0)\n {\n // if you need documentation on the next two lines, look inside the if (total.newMostToTake < 0) just above\n payload = await addSlices(pile, jobAddress);\n mostToTake = mostToTake / dcpConfig.job.uploadIncreaseFactor;\n }\n }\n\n // and finally assign whatever mostToTake was at the end of this run of the function to be returned \n // as part of the payload in case addSlices was called recursively\n payload.mostToTake = mostToTake;\n\n // cleanup connection when done with this function\n if (uploadConnection && uploadRefs === 1) {\n debugging('slice-upload') && console.debug(`250: closing uploadConnection`, uploadConnection?.id);\n await uploadConnection.close();\n debugging('slice-upload') && console.debug(`252: closed uploadConnection`, uploadConnection);\n }\n uploadRefs--;\n\n /* contains the job's lastSliceNumber (the only externally-meaningful value returned from \n * the uploading of slices to the scheduler) in case the calling function needs it \n */\n return payload;\n}\n\n/**\n * marshal the value using kvin or instance of the kvin (tunedKvin)\n * tunedKvin is defined if job.tuning.kvin is specified.\n *\n * @param {any} value \n * @return {object} A marshaled object\n * \n */\nfunction kvinMarshal (value) {\n if (tunedKvin)\n return tunedKvin.marshal(value);\n\n return kvin.marshal(value);\n}\n\n\nexports.addSlices = addSlices;\n\n\n//# sourceURL=webpack://dcp/./src/dcp-client/job/upload-slices.js?");
4213
4262
 
4214
4263
  /***/ }),
4215
4264
 
@@ -4219,7 +4268,7 @@ eval("/**\n * @file job/upload-slices.js\n * @author Ryan Saweczko,
4219
4268
  \****************************************/
4220
4269
  /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
4221
4270
 
4222
- eval("/**\n * @file range-object.js\n * @author Eddie Roosenmaallen, eddie@kingsds.network\n * Matthew Palma, mpalma@kingsds.network\n * @date October 2018\n * This module exports classes and methods for working with RangeObjects\n *\n * RangeObject:\n * new RangeObject(start, end[, step[, group]])\n * new RangeObject({ start, end[, step[, group]] })\n * new RangeObject('[object RangeObject {start},{end},{step},{group}]')\n *\n * In the first two forms, step and group are optional, defaulting to 1. In the string form, all paramteres are required\n *\n *\n * Methods:\n * toString() Returns a string descriptor, suitable to pass to a new constructor\n * toObject() Returns a vanilla ES object with the four parameters as properties\n * toArray() Resolve all values from the RangeObject, grouping if group > 1, as an array\n * nthValue(n) Look up a single value/group\n * slice([start[, end]]) Resolve a selection of values from start to end (exclusive).\n * Same semantics as Array.prototype.slice, including negative indexes\n * materialize(now) Resolve all values and store as an array (eg. for random distributions)\n *\n * Properties:\n * length Return the total number of values (or groups, if group > 1) generasted by the RangeObject\n * dist Name of the distributor function\n * values If present, the Distribution has been materialized\n * materializeOnScheduler If present and truthy, the distribution will be materialized on the scheduler before\n * instantiating tasks\n *\n * Examples:\n * bob = new RangeObject(1, 10)\n * bob.toArray() // [1,2,3,4,5,6,7,8,9,10]\n * bob.slice(-2) // [9,10]\n * fred = new RangeObject({ start:1, end: 1000, step: 3})\n * fred.toArray() // [1,4,7 (...) 994, 997, 1000]\n * fred.toObject() // { start: 1, end: 1000, step: 3, group: 1}\n * bill = new RO.RangeObject(0, 999, 3, 3)\n * rangeobject.js?1541083657962:42 Uncaught RangeError: Range must be divisible evenly into groups\n * at new RangeObject (rangeobject.js?1541083657962:42)\n * at <anonymous>:1:8\n * bill = new RO.RangeObject(0, 998, 3, 3)\n * bill.toArray() // [[0, 3, 6] (...), [990, 993, 996]]\n *\n * MultiRangeObject:\n * new MultiRangeObject(rol1, rol2, ... roln)\n * new MultiRangeObject([rol1, rol2, ... roln])\n * Each argument is either a RangeObject, an Array-like object (, or a Distribution object? NYI ~ER). The\n * values generated by the MRO will be a multi-dimentional array, where each element of the array is a vector\n * across all input ranges.\n */\n\n\n// Some utility functions to make validating the parameters easier\nconst assert = (premise, msg) => {\n if (!premise) throw new Error(msg);\n}\n\nconst assertNumeric = (val, msg) => {\n assert(typeof val === 'number', msg);\n assert(!Number.isNaN(val), msg);\n assert(Number.isFinite(val), msg);\n}\n\nconst RANGEOBJ_REGEX = /^\\[object RangeObject (-?\\d+(?:\\.\\d+)?),(-?\\d+(?:\\.\\d+)?),(-?\\d+(?:\\.\\d+)?),(\\d+|undefined)\\]$/;\n\n/** \n * Defines a consistent interface for each of the range object types\n * to inherit from, provides some array methods.\n * @access public\n */\nclass SuperRangeObject {\n constructor() {\n return new Proxy(this, {\n get: (target, name) => {\n if ((typeof name === 'string' || typeof name === 'number') && Number.isInteger(parseFloat(name))) {\n return target.nthValue(parseFloat(name));\n } else {\n return target[name];\n }\n }\n });\n }\n\n [Symbol.iterator]() {\n let index = 0;\n\n return {\n next: () => ({ value: this.nthValue(index++), done: index > this.length })\n };\n }\n\n get length() {\n return 0;\n }\n\n nthValue(n) {\n throw new Error(\"nthValue not overridden\");\n }\n\n toArray() {\n return this.slice()\n }\n\n /**\n * Generate array from range starting at value `start` and ending at value `end` via `nthValue`.\n * @param {number} [start=0] index to start slice\n * @param {number} [end] index to end slice, return rest of array if not provided.\n * @access public\n */\n slice(start, end) {\n if (typeof start === 'undefined') { start = 0 } else if (start < 0) { start += this.length }\n\n if (typeof end === 'undefined') { end = this.length } else if (end < 0) { end += (this.length) }\n\n if (end > this.length) { end = this.length }\n\n const arr = []\n for (let i = start; i < end; i++) { arr.push(this.nthValue(i)) }\n\n return arr\n }\n\n /**\n * Converts range to an Array and then calls `filter(...args)` on it.\n * @param {...any} args Same args as you would pass to Array#filter\n * @access public\n */\n filter(...args) {\n return this.toArray().filter(...args);\n }\n}\n\n/**\n * Range objects are vanilla ES objects used to describe value range sets for use by `compute.for()`.\n * The range must be increasing, i.e. `start` must be less than `end`.\n * Calculations made to derive the set of numbers in a range are carried out with `BigNumber`, \n * eg. arbitrary-precision, support. The numbers `Infinity` and `-Infinity` are not supported, and \n * the API does not differentiate between `+0` and `-0`.\n */\n/**\n * An object which represents a range of values.\n * @param {number|object} startOrObject Beginning of range, or object literal with `start` and `end` properties.\n * @param {number} end End of range\n * @param {number} [step=1] Step size in range\n * @param {number|undefined} [group] Groups in range\n * @access public\n * @extends SuperRangeObject\n * @example <caption>With implicit step size = 1.</caption>\n * let rangeObject = new RangeObject(0.5,3);\n * rangeObject.toArray();\n * // [ 0.5, 1.5, 2.5 ]\n * \n * @example <caption>With explicit step size.</caption>\n * let rangeObject = new RangeObject(1,9,3);\n * rangeObject.toArray();\n * // [ 1, 4, 7 ]\n * \n * @example <caption>With grouping.</caption>\n * let rangeObject = new RangeObject(1,9,3,2);\n * rangeObject.toArray();\n * // [ [ 1, 4 ], [ 7 ] ]\n */\nclass RangeObject extends SuperRangeObject {\n \n constructor (start, end, step, group) {\n super();\n if (typeof start === 'string' && start.match(RANGEOBJ_REGEX)) {\n const parts = start.match(RANGEOBJ_REGEX)\n start = {\n start: parseFloat(parts[1]),\n end: parseFloat(parts[2]),\n step: parseFloat(parts[3]),\n group: (parts[4] === 'undefined'? undefined : parseFloat(parts[4]))\n }\n }\n\n if (typeof start === 'object') {\n this.start = start.start;\n this.end = start.end;\n this.step = start.step;\n this.group = start.group;\n } else {\n this.start = start;\n this.end = end;\n this.step = step;\n this.group = group;\n }\n\n assertNumeric(this.start, `Invalid start parameter \"${this.start}\", must be numeric and finite.`);\n assertNumeric(this.end, `Invalid end parameter \"${this.end}\", must be numeric and finite.`);\n\n // Ensure step moves in the correct direction for start-end (ie, negative if end < start)\n if (typeof this.step === 'undefined') {\n this.step = Math.sign(this.end - this.start);\n } else {\n assertNumeric(this.step, `Invalid step parameter \"${this.step}\", must be numeric and finite.`);\n if ((this.step === 0 && this.start !== this.end) || Math.sign(this.step) !== Math.sign(this.end - this.start)) {\n throw new Error('RangeObject step must approach end from start.');\n }\n }\n\n if (typeof this.group !== 'undefined') {\n // group must remain undefined if not provided because no grouping should occur if not provided.\n // As per spec, even if group is 1 it should group into arrays of 1 element\n assertNumeric(this.group, `Invalid group parameter \"${this.group}\", must be numeric and finite.`);\n assert(Number.isInteger(this.group), `Invalid group parameter \"${this.group}\", must be an integer.`);\n assert(this.group > 0, `Invalid group parameter \"${this.group}\", must be greater than zero.`);\n }\n }\n\n /**\n * @typedef {object} RangeLike\n * @property {number} start\n * @property {number} end\n * @property {number} nthValue\n */\n\n /**\n * @returns {boolean}\n */\n static isRangelike (r) {\n if (r instanceof RangeObject) { return true }\n if (typeof r === 'object' &&\n typeof r.start === 'number' &&\n typeof r.end === 'number' &&\n typeof r.nthValue === 'function') { return true }\n\n return false\n }\n\n /**\n * @returns {boolean}\n */\n static isRangeObject(r) {\n if (r instanceof RangeObject) { return true }\n\n return false\n }\n\n /**\n * Test whether a value can be passed to the RangeObject constructor\n * @param r Value to test\n * @param strict Optional. Truthy to disallow objects which already look Rangelike\n */\n static isProtoRangelike (r, strict = true) {\n if (typeof r === 'object' &&\n typeof r.start === 'number' &&\n typeof r.end === 'number') { return true }\n\n if (typeof r === 'string' &&\n r.match(RANGEOBJ_REGEX)) { return true }\n\n if (!strict && RangeObject.isRangelike(r)) { return true }\n\n return false\n }\n\n /**\n * Create string representation of range: [object RangeObject start,end,step,group]\n * @access public\n */\n toString () {\n return `[object RangeObject ${this.start},${this.end},${this.step},${this.group}]`\n }\n\n /**\n * Create object literal for range with properties: start, end, step, and group.\n * @access public\n */\n toObject () {\n return {\n start: this.start,\n end: this.end,\n step: this.step,\n group: this.group\n }\n }\n\n /**\n * Return nth value in range\n * @param {number} n\n * @access public\n * @example\n * let rangeObject = new RangeObject(1,10,3,2);\n * rangeObject.toArray();\n * // [ [ 1, 4 ], [ 7, 10 ] ]\n * rangeObject.nthValue(1);\n * // [ 7, 10 ]\n */\n nthValue(n) {\n /**\n * `>=` since the value at index 7 in an array that's of length 7 is outside\n * its range\n */\n if (n < 0 || n >= this.length) {\n return undefined;\n }\n\n if (typeof this.group !== 'undefined') {\n const start = (this.group * this.step * n) + this.start\n const arr = []\n\n for (let i = 0; i < this.group && i + this.group * n < this.stepCount; i++) {\n arr.push(start + (i * this.step))\n }\n\n return arr;\n }\n\n return this.start + (n * this.step);\n }\n\n /**\n * Return max value in range\n * @access public\n * @returns {number}\n * @example\n * let rangeObject = new RangeObject(1,10,3,2);\n * rangeObject.toArray();\n * // [ [ 1, 4 ], [ 7, 10 ] ]\n * rangeObject.max;\n * // 10\n */\n get max () {\n if (typeof this.group === 'undefined' && this.step === 1) { return this.end }\n\n let tail\n if (typeof this.group === 'undefined') {\n [ tail ] = this.slice(-1)\n } else {\n [ tail ] = this.slice(-1)[0].slice(-1)\n }\n return tail\n }\n\n /**\n * Boolean indicating whether all groups are filled.\n * Only defined for RangeObjects that group results.\n * @access public\n * @returns {boolean}\n * @example <caption>With remainder</caption>\n * let rangeObject = new RangeObject(1,9,3,2);\n * rangeObject.toArray();\n * // [ [ 1, 4 ], [ 7 ] ]\n * rangeObject.hasRemainder;\n * // true\n * @example <caption>Without remainder</caption>\n * let rangeObject = new RangeObject(1,10,3,2);\n * rangeObject.toArray();\n * // [ [ 1, 4 ], [ 7, 10 ] ]\n * rangeObject.hasRemainder;\n * // true\n */\n get hasRemainder () {\n if (typeof this.group === 'undefined') { return false }\n const groups = this.stepCount / this.group;\n\n return (groups !== Math.floor(groups));\n }\n\n /**\n * Number of elements in range, or number of groups if grouped.\n * @access public\n * @returns {number}\n * @example <caption>Without grouping</caption>\n * let rangeObject = new RangeObject(1,10,3);\n * rangeObject.toArray();\n * // [ 1, 4, 7, 10 ]\n * rangeObject.length;\n * // 4\n * @example <caption>With grouping</caption>\n * let rangeObject = new RangeObject(1,9,3,2);\n * rangeObject.toArray();\n * // [ [ 1, 4 ], [ 7 ] ]\n * rangeObject.length;\n * // 2\n */\n get length () {\n return Math.ceil(this.stepCount / (this.group || 1));\n }\n\n /**\n * Number of steps in range (sort of like number of elements, except grouping is no longer relevant).\n * @access public\n * @returns {number}\n * @example <caption>Without grouping</caption>\n * let rangeObject = new RangeObject(1,10,3);\n * rangeObject.toArray();\n * // [ 1, 4, 7, 10 ]\n * rangeObject.stepCount;\n * // 4\n * @example <caption>With grouping</caption>\n * let rangeObject = new RangeObject(1,9,3,2);\n * rangeObject.toArray();\n * // [ [ 1, 4 ], [ 7 ] ]\n * rangeObject.length;\n * // 3\n */\n get stepCount () {\n if (this.step === 0) return 1;\n return Math.floor(Math.abs((this.end - this.start) / this.step)) + 1;\n }\n}\n\n/**\n * A sparse range object contains many {@link RangeObject}s. The range objects are stored as arrays and then are\n * concatenated into one array in the order that they were supplied to the constructor.\n * @param {RangeObject|RangeObject[]|object} arg - First range object, or array of range objects, or object with `sparse` key containing an array of range objects.\n * @param {RangeObject} rangeObject - If first argument is a RangeObject, subsquent arguments are range objects too.\n * @access public\n * @extends SuperRangeObject\n * @example\n * r0 = new RangeObject(1,2)\n * r1 = new RangeObject(1,3)\n * sro = new SparseRangeObject(r0, r1)\n * // [ 1, 2, 1, 2, 3]\n */\nclass SparseRangeObject extends SuperRangeObject {\n constructor() {\n super();\n let sparse = [];\n\n if (arguments.length === 1 && arguments[0].ranges) \n sparse = [...arguments[0].ranges];\n else \n sparse = [...arguments];\n \n if (sparse instanceof SparseRangeObject)\n throw new Error('Argument is of type sparse range object');\n \n // If sparse key is passed, make sure the arguments are only the range objects (compute.for() implementation) \n if (sparse[0].sparse)\n sparse = sparse[0].sparse;\n \n sparse.map( r =>\n {\n if (!RangeObject.isProtoRangelike(r))\n throw new Error('Argument is not of type RangeObject');\n })\n \n if (sparse[0].group)\n {\n for (let i = 0; i < sparse.length; i++)\n {\n if (sparse[i].group !== sparse[0].group)\n throw new Error('Range Object has different dimension than other range objects');\n }\n } \n \n this.sparse = true;\n this.ranges = sparse.map(r => \n { \n return new RangeObject(r);\n })\n \n }\n \n /**\n * Test whether a value can be passed to the SparseRangeObject constructor\n * @param r Value to test\n * @param strict Optional. Truthy to disallow objects which already look Rangelike\n */\n static isProtoSparseRangelike (r, strict = true) \n {\n if (typeof r === 'object' && r.sparse) { return true; }\n return false;\n }\n \n /**\n * Return nth value in range\n * @param {number} n\n * @access public\n * @example\n * let sparseRangeObject = new SparseRangeObject(1,3,1);\n * rangeObject.toArray();\n * // [ 1, 2, 3]\n * rangeObject.nthValue(1);\n * // 2\n */\n nthValue(n) \n {\n if (n < 0 || n >= this.length) { return undefined }\n \n let count = 0;\n let rangeCount = 0;\n while (count !== n)\n {\n if (count <= n)\n {\n for (let i = 0; i < this.ranges[rangeCount].length; i++)\n {\n if (count === n)\n return this.ranges[rangeCount][i];\n else\n count++;\n }\n }\n rangeCount++;\n }\n return this.ranges[rangeCount][0];\n }\n \n /**\n * Create object literal with `sparse` property and `range` property containing array of range objects.\n */\n toObject () {\n \n const obj = {\n sparse: true,\n ranges: this.ranges\n }\n return obj;\n }\n \n get length()\n {\n let len = 0;\n this.ranges.forEach((r) => len += r.length);\n\n return len;\n }\n \n}\n\n/**\n * Range objects are vanilla ES objects used to describe value range sets for use by `compute.for()`. \n * Calculations made to derive the set of numbers in a range are carried out with `BigNumber`, \n * eg. arbitrary-precision, support. The numbers `Infinity` and `-Infinity` are not supported, and \n * the API does not differentiate between `+0` and `-0`.\n */\nclass MultiRangeObject extends SuperRangeObject {\n /**\n * A multi-range object contains many {@link RangeObject}s. They are iterated over \n * with the fastest moving index going over the right-most range object in array order. Each element\n * of a multi range is a tuple of values from constituent ranges.\n * @param {RangeObject|RangeObject[]|object} arg - First range object, or array of range objects, or object with `ranges` key containing an array of range objects.\n * @param {RangeObject} rangeObject - If first argument is a RangeObject, subsquent arguments are range objects too.\n * @access public\n * @extends SuperRangeObject\n * @example\n * r0 = new RangeObject(1,2)\n * r1 = new RangeObject(1,3)\n * mro = new MultiRangeObject(r0, r1)\n * mro.toArray()\n * // [ [ 1, 1 ], [ 1, 2 ], [ 1, 3 ], [ 2, 1 ], [ 2, 2 ], [ 2, 3 ] ]\n */\n constructor () {\n super();\n var ranges = []\n\n if (arguments.length === 1 && typeof arguments[0] === 'string') {\n const inputs = JSON.parse(arguments[0])\n if (Array.isArray(inputs)) {\n ranges = inputs\n } else if (inputs.ranges) {\n ranges = inputs.ranges\n } else {\n ranges = [inputs]\n }\n } else if (arguments.length === 1 && Array.isArray(arguments[0])) {\n ranges = [...arguments[0]]\n } else if (arguments.length === 1 && !!arguments[0].ranges) {\n ranges = [...arguments[0].ranges]\n } else {\n ranges = [...arguments]\n }\n\n this.ranges = ranges.map(r => {\n if (RangeObject.isRangelike(r)) { return r }\n if (RangeObject.isRangeObject(r)) { return r }\n if (DistributionRange.isDistribution(r)) { return r }\n if (RangeObject.isProtoRangelike(r)) { return new RangeObject(r) }\n if (DistributionRange.isProtoDistribution(r)) { return new DistributionRange(r) }\n\n return Array.isArray(r) ? r : [r]\n })\n }\n\n /**\n * Test whether a value can be passed to the MultiRangeObject constructor\n * @param r Value to test\n * @param strict Optional. Truthy to disallow objects which already look Rangelike\n */\n static isProtoMultiRangelike (r, strict = true) {\n if ((typeof r === 'object') &&\n Array.isArray(r.ranges) &&\n !r.sparse)\n {\n return true;\n }\n\n return false\n }\n\n /**\n * Create string representation of this MultiRangeObject\n * @access public\n * @example\n * \"[object MultiRangeObject ' + this.ranges.length + ']\"\n */\n toString () {\n return '[object MultiRangeObject ' + this.ranges.length + ']'\n }\n\n /**\n * Create object literal with `ranges` property containing array of range objects.\n */\n toObject () {\n return { ranges: this.ranges }\n }\n\n /**\n * Returns a tuple of values from the ranges given by this multi range object.\n * @param {number} n index of multi-range tuple to return\n * @access public\n * @example\n * r0 = new RangeObject(1,2)\n * r1 = new RangeObject(1,3)\n * mro = new MultiRangeObject(r0, r1)\n * mro.toArray()\n * // [ [ 1, 1 ], [ 1, 2 ], [ 1, 3 ], [ 2, 1 ], [ 2, 2 ], [ 2, 3 ] ]\n * mro.nthValue(2)\n * // [ 1, 3 ]\n */\n nthValue (n) {\n if (n < 0 || n >= this.length) { return undefined }\n\n const indexes = []\n\n for (let r = (this.ranges.length - 1); r >= 0; r--) {\n const idx = n % this.ranges[r].length\n\n indexes.unshift(idx)\n\n n -= idx\n n /= this.ranges[r].length\n }\n\n const values = []\n\n for (let i = 0; i < indexes.length; i++) {\n values[i] = Array.isArray(this.ranges[i]) ? this.ranges[i][indexes[i]] : this.ranges[i].nthValue(indexes[i])\n }\n\n return values\n }\n\n /**\n * Boolean indicating whether any of the ranges in this multi-range object has a remainder. See {@link RangeObject#hasRemainder}.\n * @access public\n * @returns {boolean}\n */\n get hasRemainder () {\n for (let r of this.ranges) {\n if (r.hasRemainder) { return true }\n }\n\n return false\n }\n\n get length () {\n let len = 1\n\n this.ranges.forEach((r) => len *= r.length)\n\n return len\n }\n}\n\n// DistributionRange object wraps a distributing function into a RangeObject-like API\n// which can be dropped directly into a MultiRangeObject to generate input slices\nclass DistributionRange extends SuperRangeObject {\n constructor (n, dist, ...params) {\n super();\n\n this.distributor = (__webpack_require__(/*! ./stats-ranges */ \"./src/dcp-client/stats-ranges.js\").distributor);\n\n // If argv[0] is a string formatted as DistributionRange.toString(), then unpack it\n if (typeof n === 'string' && n.match(/^\\[object DistributionRange (\\w+?)\\((\\d+?)(?:,(.+?))?\\)\\]$/)) {\n const parts = n.match(/^\\[object DistributionRange (\\w+?)\\((\\d+?)(?:,(.+?))?\\)\\]$/)\n dist = parts[1]\n n = parseInt(parts[2])\n params = (parts[3] || '').split(',').map(e => parseFloat(e))\n }\n\n // If argv[0] is a string describing a DistributionRange, then unpack it\n if (typeof n === 'string' && n.match(/^(\\w+?)\\((\\d+?)(?:,(.+?))?\\)$/)) {\n const parts = n.match(/^(\\w+?)\\((\\d+?)(?:,(.+?))?\\)$/)\n dist = parts[1]\n n = parseInt(parts[2])\n params = (parts[3] || '').split(',').map(e => parseFloat(e))\n }\n\n // If argv[0] is a object of the right shape, then unpack it\n if (typeof n === 'object' &&\n typeof n.length === 'number' &&\n typeof n.dist === 'string' &&\n Array.isArray(n.params)) {\n // console.log('Unpacking proto-object', n)\n dist = n.dist\n params = n.params\n n = n.length\n if (Array.isArray(n.values)) { this.values = n.values }\n if (typeof n.materializeOnScheduler === 'boolean') { this.materializeOnScheduler = n.materializeOnScheduler }\n }\n\n Object.defineProperty(this, 'length', {\n value: n,\n enumerable: true\n })\n Object.defineProperty(this, 'dist', {\n value: dist,\n enumerable: true\n })\n Object.defineProperty(this, 'params', {\n value: params || [],\n enumerable: true\n })\n\n if (typeof this.distributor[dist] !== 'function') {\n // console.log({n,dist,params})\n throw new TypeError('dist param must point to an exported distributing function')\n }\n }\n\n /**\n * @returns {boolean}\n */\n static isDistribution (d) {\n return d instanceof DistributionRange\n }\n\n static isDistributionLike (d) {\n if (DistributionRange.isDistribution(d)) { return true }\n if (typeof d === 'object' &&\n typeof d.nthValue === 'function' &&\n typeof d.slice === 'function') { return true }\n\n return false\n }\n\n static isProtoDistribution (d) {\n if (typeof d === 'string' && d.match(/^\\[object DistributionRange (\\w+?)\\((\\d+?)(?:,(.+?))?\\)\\]$/)) { return true }\n if (typeof d === 'string' && d.match(/^(\\w+?)\\((\\d+?)(?:,(.+?))?\\)$/)) { return true }\n if (typeof d === 'object' &&\n typeof d.length === 'number' &&\n typeof d.dist === 'string' &&\n Array.isArray(d.params)) { return true }\n\n return false\n }\n\n toString () {\n return `[object DistributionRange ${this.dist}(${[this.length, ...this.params].join()})]`\n }\n\n toObject () {\n this.materialize();\n return {\n length: this.length,\n dist: this.dist,\n params: this.params,\n materializeOnScheduler: this.materializeOnScheduler || undefined,\n values: this.values || undefined\n }\n }\n\n nthValue (n) {\n if (n < 0 || n >= this.length) { return undefined }\n\n if (this.values) { return this.values[n] }\n\n const fn = this.distributor[this.dist]\n\n if (typeof fn === 'function') { return fn.apply(fn, [n, this.length, ...this.params]) }\n\n return undefined\n }\n\n /** Resolve the distribution to a static array\n * @param now If false, then set a flag to materialize on the scheduler. Default: materialize now\n */\n materialize (now = true) {\n if (now === false) { return this.materializeOnScheduler = true }\n\n this.values = this.toArray()\n }\n}\n\n/** Rehydrate an input range from a vanilla ES5 object to an appropriate rangelike object\n * @param obj Serialized job.data object (or JSON string)\n * @return as appropriate, a RangeObject, DistributionRange, MultiRangeObject, or array\n */\nfunction rehydrateRange (obj) {\n const { RemoteDataPattern } = __webpack_require__(/*! dcp/dcp-client/remote-data-pattern */ \"./src/dcp-client/remote-data-pattern.js\");\n const { RemoteDataSet } = __webpack_require__(/*! dcp/dcp-client/remote-data-set */ \"./src/dcp-client/remote-data-set.js\");\n\n if (typeof obj === 'string') {\n obj = JSON.parse(obj)\n }\n\n if (typeof obj === 'number') {\n return obj\n }\n\n if (obj instanceof RangeObject ||\n obj instanceof SparseRangeObject ||\n obj instanceof MultiRangeObject ||\n obj instanceof RemoteDataSet ||\n obj instanceof RemoteDataPattern ||\n obj instanceof DistributionRange) {\n return obj;\n }\n\n // If obj looks like a RemoteDataSet, make one of those\n if (RemoteDataSet.isProtoRemoteDataSetLike(obj)) {\n return new RemoteDataSet(obj)\n }\n \n // If obj looks like a RemoteDataPattern, make one of those\n if (RemoteDataPattern.isProtoRemoteDataPatternLike(obj)) {\n return new RemoteDataPattern(obj.pattern, obj.sliceCount)\n }\n\n // If obj is an iterable, coerce it to an array\n if (Symbol.iterator in Object(obj)) {\n return Array.from(obj)\n }\n \n // If obj looks like a SparseRangeObject, make one of those\n if (SparseRangeObject.isProtoSparseRangelike(obj))\n return new SparseRangeObject(obj);\n\n // If obj looks like a MultiRangeObject, make one of those\n if (MultiRangeObject.isProtoMultiRangelike(obj)) {\n return new MultiRangeObject(obj)\n }\n\n // If obj looks rangelike, make a RangeObject\n if (RangeObject.isProtoRangelike(obj)) {\n return new RangeObject(obj)\n }\n\n // If obj looks like a proto-distribution, make a DistributionRange\n if (DistributionRange.isProtoDistribution(obj)) {\n return new DistributionRange(obj)\n }\n\n throw new TypeError(`obj cannot be cast to any supported Rangelike object: ${JSON.stringify(obj)}`)\n}\n\nexports.SuperRangeObject = SuperRangeObject;\nexports.RangeObject = RangeObject;\nexports.MultiRangeObject = MultiRangeObject;\nexports.DistributionRange = DistributionRange;\nexports.SparseRangeObject = SparseRangeObject;\nexports.rehydrateRange = rehydrateRange;\n\n\n//# sourceURL=webpack://dcp/./src/dcp-client/range-object.js?");
4271
+ eval("/**\n * @file range-object.js\n * @author Eddie Roosenmaallen, eddie@kingsds.network\n * Matthew Palma, mpalma@kingsds.network\n * Robert Mirandola, robert@kingsds.network\n * @date October 2018\n * July 2022\n * This module exports classes and methods for working with RangeObjects\n *\n * RangeObject:\n * new RangeObject(start, end[, step[, group]])\n * new RangeObject({ start, end[, step[, group]] })\n * new RangeObject('[object RangeObject {start},{end},{step},{group}]')\n *\n * In the first two forms, step and group are optional, defaulting to 1. In the string form, all paramteres are required\n *\n *\n * Methods:\n * toString() Returns a string descriptor, suitable to pass to a new constructor\n * toObject() Returns a vanilla ES object with the four parameters as properties\n * toArray() Resolve all values from the RangeObject, grouping if group > 1, as an array\n * nthValue(n) Look up a single value/group\n * slice([start[, end]]) Resolve a selection of values from start to end (exclusive).\n * Same semantics as Array.prototype.slice, including negative indexes\n * materialize(now) Resolve all values and store as an array (eg. for random distributions)\n *\n * Properties:\n * length Return the total number of values (or groups, if group > 1) generasted by the RangeObject\n * dist Name of the distributor function\n * values If present, the Distribution has been materialized\n * materializeOnScheduler If present and truthy, the distribution will be materialized on the scheduler before\n * instantiating tasks\n *\n * Examples:\n * bob = new RangeObject(1, 10)\n * bob.toArray() // [1,2,3,4,5,6,7,8,9,10]\n * bob.slice(-2) // [9,10]\n * fred = new RangeObject({ start:1, end: 1000, step: 3})\n * fred.toArray() // [1,4,7 (...) 994, 997, 1000]\n * fred.toObject() // { start: 1, end: 1000, step: 3, group: 1}\n * bill = new RO.RangeObject(0, 999, 3, 3)\n * rangeobject.js?1541083657962:42 Uncaught RangeError: Range must be divisible evenly into groups\n * at new RangeObject (rangeobject.js?1541083657962:42)\n * at <anonymous>:1:8\n * bill = new RO.RangeObject(0, 998, 3, 3)\n * bill.toArray() // [[0, 3, 6] (...), [990, 993, 996]]\n *\n * MultiRangeObject:\n * new MultiRangeObject(rol1, rol2, ... roln)\n * new MultiRangeObject([rol1, rol2, ... roln])\n * Each argument is either a RangeObject, an Array-like object (, or a Distribution object? NYI ~ER). The\n * values generated by the MRO will be a multi-dimentional array, where each element of the array is a vector\n * across all input ranges.\n */\n\n\n// Some utility functions to make validating the parameters easier\nconst assert = (premise, msg) => {\n if (!premise) throw new Error(msg);\n}\n\nconst assertNumeric = (val, msg) => {\n assert(typeof val === 'number', msg);\n assert(!Number.isNaN(val), msg);\n assert(Number.isFinite(val), msg);\n}\n\nconst RANGEOBJ_REGEX = /^\\[object RangeObject (-?\\d+(?:\\.\\d+)?),(-?\\d+(?:\\.\\d+)?),(-?\\d+(?:\\.\\d+)?),(\\d+|undefined)\\]$/;\n\n/** \n * Defines a consistent interface for each of the range object types\n * to inherit from, provides some array methods.\n * @access public\n */\nclass SuperRangeObject {\n constructor() {\n return new Proxy(this, {\n get: (target, name) => {\n if ((typeof name === 'string' || typeof name === 'number') && Number.isInteger(parseFloat(name))) {\n return target.nthValue(parseFloat(name));\n } else {\n return target[name];\n }\n }\n });\n }\n\n [Symbol.iterator]() {\n let index = 0;\n\n return {\n next: () => ({ value: this.nthValue(index++), done: index > this.length })\n };\n }\n\n get length() {\n return 0;\n }\n\n nthValue(n) {\n throw new Error(\"nthValue not overridden\");\n }\n\n toArray() {\n return this.slice()\n }\n\n /**\n * Generate array from range starting at value `start` and ending at value `end` via `nthValue`.\n * @param {number} [start=0] index to start slice\n * @param {number} [end] index to end slice, return rest of array if not provided.\n * @access public\n */\n slice(start, end) {\n if (typeof start === 'undefined') { start = 0 } else if (start < 0) { start += this.length }\n\n if (typeof end === 'undefined') { end = this.length } else if (end < 0) { end += (this.length) }\n\n if (end > this.length) { end = this.length }\n\n const arr = []\n for (let i = start; i < end; i++) { arr.push(this.nthValue(i)) }\n\n return arr\n }\n\n /**\n * Converts range to an Array and then calls `filter(...args)` on it.\n * @param {...any} args Same args as you would pass to Array#filter\n * @access public\n */\n filter(...args) {\n return this.toArray().filter(...args);\n }\n}\n\n/**\n * Range objects are vanilla ES objects used to describe value range sets for use by `compute.for()`.\n * The range must be increasing, i.e. `start` must be less than `end`.\n * Calculations made to derive the set of numbers in a range are carried out with `BigNumber`, \n * eg. arbitrary-precision, support. The numbers `Infinity` and `-Infinity` are not supported, and \n * the API does not differentiate between `+0` and `-0`.\n */\n/**\n * An object which represents a range of values.\n * @param {number|object} startOrObject Beginning of range, or object literal with `start` and `end` properties.\n * @param {number} end End of range\n * @param {number} [step=1] Step size in range\n * @param {number|undefined} [group] Groups in range\n * @access public\n * @extends SuperRangeObject\n * @example <caption>With implicit step size = 1.</caption>\n * let rangeObject = new RangeObject(0.5,3);\n * rangeObject.toArray();\n * // [ 0.5, 1.5, 2.5 ]\n * \n * @example <caption>With explicit step size.</caption>\n * let rangeObject = new RangeObject(1,9,3);\n * rangeObject.toArray();\n * // [ 1, 4, 7 ]\n * \n * @example <caption>With grouping.</caption>\n * let rangeObject = new RangeObject(1,9,3,2);\n * rangeObject.toArray();\n * // [ [ 1, 4 ], [ 7 ] ]\n */\nclass RangeObject extends SuperRangeObject {\n \n constructor (start, end, step, group) {\n super();\n if (typeof start === 'string' && start.match(RANGEOBJ_REGEX)) {\n const parts = start.match(RANGEOBJ_REGEX)\n start = {\n start: parseFloat(parts[1]),\n end: parseFloat(parts[2]),\n step: parseFloat(parts[3]),\n group: (parts[4] === 'undefined'? undefined : parseFloat(parts[4]))\n }\n }\n\n if (typeof start === 'object') {\n this.start = start.start;\n this.end = start.end;\n this.step = start.step;\n this.group = start.group;\n } else {\n this.start = start;\n this.end = end;\n this.step = step;\n this.group = group;\n }\n\n assertNumeric(this.start, `Invalid start parameter \"${this.start}\", must be numeric and finite.`);\n assertNumeric(this.end, `Invalid end parameter \"${this.end}\", must be numeric and finite.`);\n\n // Ensure step moves in the correct direction for start-end (ie, negative if end < start)\n if (typeof this.step === 'undefined') {\n this.step = Math.sign(this.end - this.start);\n } else {\n assertNumeric(this.step, `Invalid step parameter \"${this.step}\", must be numeric and finite.`);\n if ((this.step === 0 && this.start !== this.end) || Math.sign(this.step) !== Math.sign(this.end - this.start)) {\n throw new Error('RangeObject step must approach end from start.');\n }\n }\n\n if (typeof this.group !== 'undefined') {\n // group must remain undefined if not provided because no grouping should occur if not provided.\n // As per spec, even if group is 1 it should group into arrays of 1 element\n assertNumeric(this.group, `Invalid group parameter \"${this.group}\", must be numeric and finite.`);\n assert(Number.isInteger(this.group), `Invalid group parameter \"${this.group}\", must be an integer.`);\n assert(this.group > 0, `Invalid group parameter \"${this.group}\", must be greater than zero.`);\n }\n }\n\n /**\n * @typedef {object} RangeLike\n * @property {number} start\n * @property {number} end\n * @property {number} nthValue\n */\n\n /**\n * @returns {boolean}\n */\n static isRangelike (r) {\n if (r instanceof RangeObject) { return true }\n if (typeof r === 'object' &&\n typeof r.start === 'number' &&\n typeof r.end === 'number' &&\n typeof r.nthValue === 'function') { return true }\n\n return false\n }\n\n /**\n * @returns {boolean}\n */\n static isRangeObject(r) {\n if (r instanceof RangeObject) { return true }\n\n return false\n }\n\n /**\n * Test whether a value can be passed to the RangeObject constructor\n * @param r Value to test\n * @param strict Optional. Truthy to disallow objects which already look Rangelike\n */\n static isProtoRangelike (r, strict = true) {\n if (typeof r === 'object' &&\n typeof r.start === 'number' &&\n typeof r.end === 'number') { return true }\n\n if (typeof r === 'string' &&\n r.match(RANGEOBJ_REGEX)) { return true }\n\n if (!strict && RangeObject.isRangelike(r)) { return true }\n\n return false\n }\n\n /**\n * Create string representation of range: [object RangeObject start,end,step,group]\n * @access public\n */\n toString () {\n return `[object RangeObject ${this.start},${this.end},${this.step},${this.group}]`\n }\n\n /**\n * Create object literal for range with properties: start, end, step, and group.\n * @access public\n */\n toObject () {\n return {\n start: this.start,\n end: this.end,\n step: this.step,\n group: this.group\n }\n }\n\n /**\n * Return nth value in range\n * @param {number} n\n * @access public\n * @example\n * let rangeObject = new RangeObject(1,10,3,2);\n * rangeObject.toArray();\n * // [ [ 1, 4 ], [ 7, 10 ] ]\n * rangeObject.nthValue(1);\n * // [ 7, 10 ]\n */\n nthValue(n) {\n /**\n * `>=` since the value at index 7 in an array that's of length 7 is outside\n * its range\n */\n if (n < 0 || n >= this.length) {\n return undefined;\n }\n\n if (typeof this.group !== 'undefined') {\n const start = (this.group * this.step * n) + this.start\n const arr = []\n\n for (let i = 0; i < this.group && i + this.group * n < this.stepCount; i++) {\n arr.push(start + (i * this.step))\n }\n\n return arr;\n }\n\n return this.start + (n * this.step);\n }\n\n /**\n * Return max value in range\n * @access public\n * @returns {number}\n * @example\n * let rangeObject = new RangeObject(1,10,3,2);\n * rangeObject.toArray();\n * // [ [ 1, 4 ], [ 7, 10 ] ]\n * rangeObject.max;\n * // 10\n */\n get max () {\n if (typeof this.group === 'undefined' && this.step === 1) { return this.end }\n\n let tail\n if (typeof this.group === 'undefined') {\n [ tail ] = this.slice(-1)\n } else {\n [ tail ] = this.slice(-1)[0].slice(-1)\n }\n return tail\n }\n\n /**\n * Boolean indicating whether all groups are filled.\n * Only defined for RangeObjects that group results.\n * @access public\n * @returns {boolean}\n * @example <caption>With remainder</caption>\n * let rangeObject = new RangeObject(1,9,3,2);\n * rangeObject.toArray();\n * // [ [ 1, 4 ], [ 7 ] ]\n * rangeObject.hasRemainder;\n * // true\n * @example <caption>Without remainder</caption>\n * let rangeObject = new RangeObject(1,10,3,2);\n * rangeObject.toArray();\n * // [ [ 1, 4 ], [ 7, 10 ] ]\n * rangeObject.hasRemainder;\n * // true\n */\n get hasRemainder () {\n if (typeof this.group === 'undefined') { return false }\n const groups = this.stepCount / this.group;\n\n return (groups !== Math.floor(groups));\n }\n\n /**\n * Number of elements in range, or number of groups if grouped.\n * @access public\n * @returns {number}\n * @example <caption>Without grouping</caption>\n * let rangeObject = new RangeObject(1,10,3);\n * rangeObject.toArray();\n * // [ 1, 4, 7, 10 ]\n * rangeObject.length;\n * // 4\n * @example <caption>With grouping</caption>\n * let rangeObject = new RangeObject(1,9,3,2);\n * rangeObject.toArray();\n * // [ [ 1, 4 ], [ 7 ] ]\n * rangeObject.length;\n * // 2\n */\n get length () {\n return Math.ceil(this.stepCount / (this.group || 1));\n }\n\n /**\n * Number of steps in range (sort of like number of elements, except grouping is no longer relevant).\n * @access public\n * @returns {number}\n * @example <caption>Without grouping</caption>\n * let rangeObject = new RangeObject(1,10,3);\n * rangeObject.toArray();\n * // [ 1, 4, 7, 10 ]\n * rangeObject.stepCount;\n * // 4\n * @example <caption>With grouping</caption>\n * let rangeObject = new RangeObject(1,9,3,2);\n * rangeObject.toArray();\n * // [ [ 1, 4 ], [ 7 ] ]\n * rangeObject.length;\n * // 3\n */\n get stepCount () {\n if (this.step === 0) return 1;\n return Math.floor(Math.abs((this.end - this.start) / this.step)) + 1;\n }\n}\n\n/**\n * A sparse range object contains many {@link RangeObject}s. The range objects are stored as arrays and then are\n * concatenated into one array in the order that they were supplied to the constructor.\n * @param {RangeObject|RangeObject[]|object} arg - First range object, or array of range objects, or object with `sparse` key containing an array of range objects.\n * @param {RangeObject} rangeObject - If first argument is a RangeObject, subsquent arguments are range objects too.\n * @access public\n * @extends SuperRangeObject\n * @example\n * r0 = new RangeObject(1,2)\n * r1 = new RangeObject(1,3)\n * sro = new SparseRangeObject(r0, r1)\n * // [ 1, 2, 1, 2, 3]\n */\nclass SparseRangeObject extends SuperRangeObject {\n constructor() {\n super();\n let sparse = [];\n\n if (arguments.length === 1 && arguments[0].ranges) \n sparse = [...arguments[0].ranges];\n else \n sparse = [...arguments];\n \n if (sparse instanceof SparseRangeObject)\n throw new Error('Argument is of type sparse range object');\n \n // If sparse key is passed, make sure the arguments are only the range objects (compute.for() implementation) \n if (sparse[0].sparse)\n sparse = sparse[0].sparse;\n \n sparse.map( r =>\n {\n if (!RangeObject.isProtoRangelike(r))\n throw new Error('Argument is not of type RangeObject');\n })\n \n if (sparse[0].group)\n {\n for (let i = 0; i < sparse.length; i++)\n {\n if (sparse[i].group !== sparse[0].group)\n throw new Error('Range Object has different dimension than other range objects');\n }\n } \n \n this.sparse = true;\n this.ranges = sparse.map(r => \n { \n return new RangeObject(r);\n })\n \n }\n \n /**\n * Test whether a value can be passed to the SparseRangeObject constructor\n * @param r Value to test\n * @param strict Optional. Truthy to disallow objects which already look Rangelike\n */\n static isProtoSparseRangelike (r, strict = true) \n {\n if (typeof r === 'object' && r.sparse) { return true; }\n return false;\n }\n \n /**\n * Return nth value in range\n * @param {number} n\n * @access public\n * @example\n * let sparseRangeObject = new SparseRangeObject(1,3,1);\n * rangeObject.toArray();\n * // [ 1, 2, 3]\n * rangeObject.nthValue(1);\n * // 2\n */\n nthValue(n) \n {\n if (n < 0 || n >= this.length) { return undefined }\n \n let count = 0;\n let rangeCount = 0;\n while (count !== n)\n {\n if (count <= n)\n {\n for (let i = 0; i < this.ranges[rangeCount].length; i++)\n {\n if (count === n)\n return this.ranges[rangeCount][i];\n else\n count++;\n }\n }\n rangeCount++;\n }\n return this.ranges[rangeCount][0];\n }\n \n /**\n * Create object literal with `sparse` property and `range` property containing array of range objects.\n */\n toObject () {\n \n const obj = {\n sparse: true,\n ranges: this.ranges\n }\n return obj;\n }\n \n get length()\n {\n let len = 0;\n this.ranges.forEach((r) => len += r.length);\n\n return len;\n }\n \n}\n\n/**\n * Range objects are vanilla ES objects used to describe value range sets for use by `compute.for()`. \n * Calculations made to derive the set of numbers in a range are carried out with `BigNumber`, \n * eg. arbitrary-precision, support. The numbers `Infinity` and `-Infinity` are not supported, and \n * the API does not differentiate between `+0` and `-0`.\n */\nclass MultiRangeObject extends SuperRangeObject {\n /**\n * A multi-range object contains many {@link RangeObject}s. They are iterated over \n * with the fastest moving index going over the right-most range object in array order. Each element\n * of a multi range is a tuple of values from constituent ranges.\n * @param {RangeObject|RangeObject[]|object} arg - First range object, or array of range objects, or object with `ranges` key containing an array of range objects.\n * @param {RangeObject} rangeObject - If first argument is a RangeObject, subsquent arguments are range objects too.\n * @access public\n * @extends SuperRangeObject\n * @example\n * r0 = new RangeObject(1,2)\n * r1 = new RangeObject(1,3)\n * mro = new MultiRangeObject(r0, r1)\n * mro.toArray()\n * // [ [ 1, 1 ], [ 1, 2 ], [ 1, 3 ], [ 2, 1 ], [ 2, 2 ], [ 2, 3 ] ]\n */\n constructor () {\n super();\n var ranges = []\n\n if (arguments.length === 1 && typeof arguments[0] === 'string') {\n const inputs = JSON.parse(arguments[0])\n if (Array.isArray(inputs)) {\n ranges = inputs\n } else if (inputs.ranges) {\n ranges = inputs.ranges\n } else {\n ranges = [inputs]\n }\n } else if (arguments.length === 1 && Array.isArray(arguments[0])) {\n ranges = [...arguments[0]]\n } else if (arguments.length === 1 && !!arguments[0].ranges) {\n ranges = [...arguments[0].ranges]\n } else {\n ranges = [...arguments]\n }\n\n this.ranges = ranges.map(r => {\n if (RangeObject.isRangelike(r)) { return r }\n if (RangeObject.isRangeObject(r)) { return r }\n if (DistributionRange.isDistribution(r)) { return r }\n if (RangeObject.isProtoRangelike(r)) { return new RangeObject(r) }\n if (DistributionRange.isProtoDistribution(r)) { return new DistributionRange(r) }\n\n return Array.isArray(r) ? r : [r]\n })\n }\n\n /**\n * Test whether a value can be passed to the MultiRangeObject constructor\n * @param r Value to test\n * @param strict Optional. Truthy to disallow objects which already look Rangelike\n */\n static isProtoMultiRangelike (r, strict = true) {\n if ((typeof r === 'object') &&\n Array.isArray(r.ranges) &&\n !r.sparse)\n {\n return true;\n }\n\n return false\n }\n\n /**\n * Create string representation of this MultiRangeObject\n * @access public\n * @example\n * \"[object MultiRangeObject ' + this.ranges.length + ']\"\n */\n toString () {\n return '[object MultiRangeObject ' + this.ranges.length + ']'\n }\n\n /**\n * Create object literal with `ranges` property containing array of range objects.\n */\n toObject () {\n return { ranges: this.ranges }\n }\n\n /**\n * Returns a tuple of values from the ranges given by this multi range object.\n * @param {number} n index of multi-range tuple to return\n * @access public\n * @example\n * r0 = new RangeObject(1,2)\n * r1 = new RangeObject(1,3)\n * mro = new MultiRangeObject(r0, r1)\n * mro.toArray()\n * // [ [ 1, 1 ], [ 1, 2 ], [ 1, 3 ], [ 2, 1 ], [ 2, 2 ], [ 2, 3 ] ]\n * mro.nthValue(2)\n * // [ 1, 3 ]\n */\n nthValue (n) {\n if (n < 0 || n >= this.length) { return undefined }\n\n const indexes = []\n\n for (let r = (this.ranges.length - 1); r >= 0; r--) {\n const idx = n % this.ranges[r].length\n\n indexes.unshift(idx)\n\n n -= idx\n n /= this.ranges[r].length\n }\n\n const values = []\n\n for (let i = 0; i < indexes.length; i++) {\n values[i] = Array.isArray(this.ranges[i]) ? this.ranges[i][indexes[i]] : this.ranges[i].nthValue(indexes[i])\n }\n\n return values\n }\n\n /**\n * Boolean indicating whether any of the ranges in this multi-range object has a remainder. See {@link RangeObject#hasRemainder}.\n * @access public\n * @returns {boolean}\n */\n get hasRemainder () {\n for (let r of this.ranges) {\n if (r.hasRemainder) { return true }\n }\n\n return false\n }\n\n get length () {\n let len = 1\n\n this.ranges.forEach((r) => len *= r.length)\n\n return len\n }\n}\n\n// DistributionRange object wraps a distributing function into a RangeObject-like API\n// which can be dropped directly into a MultiRangeObject to generate input slices\nclass DistributionRange extends SuperRangeObject {\n constructor (n, dist, ...params) {\n super();\n\n this.distributor = (__webpack_require__(/*! ./stats-ranges */ \"./src/dcp-client/stats-ranges.js\").distributor);\n\n // If argv[0] is a string formatted as DistributionRange.toString(), then unpack it\n if (typeof n === 'string' && n.match(/^\\[object DistributionRange (\\w+?)\\((\\d+?)(?:,(.+?))?\\)\\]$/)) {\n const parts = n.match(/^\\[object DistributionRange (\\w+?)\\((\\d+?)(?:,(.+?))?\\)\\]$/)\n dist = parts[1]\n n = parseInt(parts[2])\n params = (parts[3] || '').split(',').map(e => parseFloat(e))\n }\n\n // If argv[0] is a string describing a DistributionRange, then unpack it\n if (typeof n === 'string' && n.match(/^(\\w+?)\\((\\d+?)(?:,(.+?))?\\)$/)) {\n const parts = n.match(/^(\\w+?)\\((\\d+?)(?:,(.+?))?\\)$/)\n dist = parts[1]\n n = parseInt(parts[2])\n params = (parts[3] || '').split(',').map(e => parseFloat(e))\n }\n\n // If argv[0] is a object of the right shape, then unpack it\n if (typeof n === 'object' &&\n typeof n.length === 'number' &&\n typeof n.dist === 'string' &&\n Array.isArray(n.params)) {\n // console.log('Unpacking proto-object', n)\n dist = n.dist\n params = n.params\n n = n.length\n if (Array.isArray(n.values)) { this.values = n.values }\n if (typeof n.materializeOnScheduler === 'boolean') { this.materializeOnScheduler = n.materializeOnScheduler }\n }\n\n Object.defineProperty(this, 'length', {\n value: n,\n enumerable: true\n })\n Object.defineProperty(this, 'dist', {\n value: dist,\n enumerable: true\n })\n Object.defineProperty(this, 'params', {\n value: params || [],\n enumerable: true\n })\n\n if (typeof this.distributor[dist] !== 'function') {\n // console.log({n,dist,params})\n throw new TypeError('dist param must point to an exported distributing function')\n }\n }\n\n /**\n * @returns {boolean}\n */\n static isDistribution (d) {\n return d instanceof DistributionRange\n }\n\n static isDistributionLike (d) {\n if (DistributionRange.isDistribution(d)) { return true }\n if (typeof d === 'object' &&\n typeof d.nthValue === 'function' &&\n typeof d.slice === 'function') { return true }\n\n return false\n }\n\n static isProtoDistribution (d) {\n if (typeof d === 'string' && d.match(/^\\[object DistributionRange (\\w+?)\\((\\d+?)(?:,(.+?))?\\)\\]$/)) { return true }\n if (typeof d === 'string' && d.match(/^(\\w+?)\\((\\d+?)(?:,(.+?))?\\)$/)) { return true }\n if (typeof d === 'object' &&\n typeof d.length === 'number' &&\n typeof d.dist === 'string' &&\n Array.isArray(d.params)) { return true }\n\n return false\n }\n\n toString () {\n return `[object DistributionRange ${this.dist}(${[this.length, ...this.params].join()})]`\n }\n\n toObject () {\n this.materialize();\n return {\n length: this.length,\n dist: this.dist,\n params: this.params,\n materializeOnScheduler: this.materializeOnScheduler || undefined,\n values: this.values || undefined\n }\n }\n\n nthValue (n) {\n if (n < 0 || n >= this.length) { return undefined }\n\n if (this.values) { return this.values[n] }\n\n const fn = this.distributor[this.dist]\n\n if (typeof fn === 'function') { return fn.apply(fn, [n, this.length, ...this.params]) }\n\n return undefined\n }\n\n /** Resolve the distribution to a static array\n * @param now If false, then set a flag to materialize on the scheduler. Default: materialize now\n */\n materialize (now = true) {\n if (now === false) { return this.materializeOnScheduler = true }\n\n this.values = this.toArray()\n }\n}\n\n/** Rehydrate an input range from a vanilla ES5 object to an appropriate rangelike object\n * @param obj Serialized job.data object (or JSON string)\n * @return as appropriate, a RangeObject, DistributionRange, MultiRangeObject, or array\n */\nfunction rehydrateRange (obj) {\n const { RemoteDataPattern } = __webpack_require__(/*! dcp/dcp-client/remote-data-pattern */ \"./src/dcp-client/remote-data-pattern.js\");\n const { RemoteDataSet } = __webpack_require__(/*! dcp/dcp-client/remote-data-set */ \"./src/dcp-client/remote-data-set.js\");\n\n if (typeof obj === 'string') {\n obj = JSON.parse(obj)\n }\n\n if (typeof obj === 'number') {\n return obj\n }\n\n if (obj instanceof RangeObject ||\n obj instanceof SparseRangeObject ||\n obj instanceof MultiRangeObject ||\n obj instanceof RemoteDataSet ||\n obj instanceof RemoteDataPattern ||\n obj instanceof DistributionRange) {\n return obj;\n }\n\n // If obj looks like a RemoteDataSet, make one of those\n if (RemoteDataSet.isProtoRemoteDataSetLike(obj)) {\n return new RemoteDataSet(obj)\n }\n \n // If obj looks like a RemoteDataPattern, make one of those\n if (RemoteDataPattern.isProtoRemoteDataPatternLike(obj)) {\n return new RemoteDataPattern(obj.pattern, obj.sliceCount)\n }\n\n // If obj is an iterable, coerce it to an array\n if (Symbol.iterator in Object(obj)) {\n return Array.from(obj)\n }\n \n // If obj looks like a SparseRangeObject, make one of those\n if (SparseRangeObject.isProtoSparseRangelike(obj))\n return new SparseRangeObject(obj);\n\n // If obj looks like a MultiRangeObject, make one of those\n if (MultiRangeObject.isProtoMultiRangelike(obj)) {\n return new MultiRangeObject(obj)\n }\n\n // If obj looks rangelike, make a RangeObject\n if (RangeObject.isProtoRangelike(obj)) {\n return new RangeObject(obj)\n }\n\n // If obj looks like a proto-distribution, make a DistributionRange\n if (DistributionRange.isProtoDistribution(obj)) {\n return new DistributionRange(obj)\n }\n\n throw new TypeError(`obj cannot be cast to any supported Rangelike object: ${JSON.stringify(obj)}`)\n}\n\nexports.SuperRangeObject = SuperRangeObject;\nexports.RangeObject = RangeObject;\nexports.MultiRangeObject = MultiRangeObject;\nexports.DistributionRange = DistributionRange;\nexports.SparseRangeObject = SparseRangeObject;\nexports.rehydrateRange = rehydrateRange;\n\n\n//# sourceURL=webpack://dcp/./src/dcp-client/range-object.js?");
4223
4272
 
4224
4273
  /***/ }),
4225
4274
 
@@ -4269,7 +4318,13 @@ eval("/* provided dependency */ var process = __webpack_require__(/*! ./node_mod
4269
4318
  \*************************************************/
4270
4319
  /***/ ((module, __unused_webpack_exports, __webpack_require__) => {
4271
4320
 
4272
- eval("/**\n * @file /src/schedmsg/schedmsg-web.js\n * @author Ryan Rossiter, ryan@kingsds.network\n * @date March 2020\n *\n * This is the SchedMsg implementation for commands that are browser-specific\n * or have browser-specific behaviour.\n */\n\nconst { SchedMsg } = __webpack_require__(/*! ./schedmsg */ \"./src/dcp-client/schedmsg/schedmsg.js\");\n\nclass SchedMsgWeb extends SchedMsg {\n constructor(worker) {\n super(worker);\n this.modal = null;\n\n this.registerHandler('announce', this.onAnnouncement.bind(this));\n this.registerHandler('openPopup', this.onOpenPopup.bind(this));\n this.registerHandler('reload', this.onReload.bind(this));\n }\n\n onAnnouncement({ message }) {\n if (this.modal) {\n this.modal.close();\n }\n\n this.modal = window.userInterface.alert('Announcement', '' /* subtitle */, message,\n /* onClose */ () => this.modal = null);\n }\n\n onOpenPopup({ href }) {\n window.open(href);\n }\n\n onReload() {\n const hash = window.location.hash;\n\n let newUrl = window.location.href.replace(/#.*/, '');\n newUrl += (newUrl.indexOf('?') === -1 ? '?' : '&');\n newUrl += 'dcp=b5864d51c1e7a9b3024698306574b60003a91d62,' + Date.now() + hash;\n\n window.location.replace(newUrl);\n }\n}\n\nObject.assign(module.exports, {\n SchedMsgWeb\n});\n\n\n//# sourceURL=webpack://dcp/./src/dcp-client/schedmsg/schedmsg-web.js?");
4321
+ <<<<<<< HEAD
4322
+ 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=4dd484422936278679ed70ce7501d11964b9d792,' + 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?");
4323
+ ||||||| e0e85dd
4324
+ 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=057ccd920bfd464401288eed9b98f438ace69b2e,' + 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?");
4325
+ =======
4326
+ 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=d4f755b478f851a75b532e258d99ea2c45d288fb,' + 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?");
4327
+ >>>>>>> origin/prod-20220907
4273
4328
 
4274
4329
  /***/ }),
4275
4330
 
@@ -4279,7 +4334,7 @@ eval("/**\n * @file /src/schedmsg/schedmsg-web.js\n * @author Ryan Rossi
4279
4334
  \*********************************************/
4280
4335
  /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
4281
4336
 
4282
- eval("/**\n * @file /src/schedmsg/schedmsg.js\n * @author Ryan Rossiter, ryan@kingsds.network\n * @date March 2020\n *\n * This is the client logic for subscribing to and handling SchedMsg events.\n * It inherits from PropagatingEventEmitter, and will register default event\n * callbacks for the commands, but they can be overridden by registering\n * additional event callbacks that return 'false'. This will prevent the default\n * behaviour from occuring.\n */\n\nconst { EventSubscriber } = __webpack_require__(/*! dcp/events/event-subscriber */ \"./src/events/event-subscriber.js\");\nconst { PropagatingEventEmitter } = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\");\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\nconst protocolV4 = __webpack_require__(/*! dcp/protocol-v4 */ \"./src/protocol-v4/index.js\");\n\n/** @typedef {import('../worker').Worker} Worker */\n\n/**\n * SchedMsg is the class that will subscribe to receive commands from the scheduler.\n * These commands could be used to reload the worker, or broacast a message.\n * \n * SchedMsg extends the PropagatingEventEmitter.\n * When a command is received from the scheduler, it will be emitted on the Schedsg\n * instance. SchedMsg will register default handlers for these commands, but they can\n * be overridden by listening for the event and returning `false` from callback you provide.\n * @access public\n */\nclass SchedMsg extends PropagatingEventEmitter {\n /**\n * This command instructs the worker to immediately stop working, and can optionally disable the worker to prevent restarting. The user will need to manually intervene to restart the worker.\n * \n * @event SchedMsg#kill\n * @access public\n * @type {object}\n * @property {boolean} temporary When false, the worker will be disabled.\n *//**\n * This command instructs the worker to restart, e.g. call `worker.stop()` then `worker.start()`.\n * \n * @event SchedMsg#restart\n * @access public\n *//**\n * This command instructs the worker to stop working on a specific job.\n * \n * @event SchedMsg#remove\n * @access public\n * @type {object}\n * @property {string} jobAddress The address of the job to stop working on.\n *//**\n * This command is an announcement from the scheduler, the provided message should be displayed to the user (modal on web, console on node).\n * \n * @event SchedMsg#announce\n * @access public\n * @type {object}\n * @property {string} message The message to be displayed to the user.\n *//**\n * This command instructs the worker to \"hard\" reload, in the browser this will trigger a page refresh and in node it will exit the process.\n * \n * @event SchedMsg#reload\n * @access public\n *//**\n * This *web-only* command will open a new webpage to the provided URL.\n * \n * @event SchedMsg#openPopup\n * @access public\n * @type {object}\n * @property {string} href The URL to open the new page to.\n */\n\n /**\n * @constructor\n * @param {Worker} worker \n */\n constructor(worker) {\n super('SchedMsg');\n\n this.eventSubscriber = new EventSubscriber(this);\n \n this.worker = worker;\n this.handlers = {};\n this.pongAttempts = 0;\n \n this.registerHandler('kill', (opts) => this.onKill(opts));\n this.registerHandler('restart', (opts) => this.onRestart(opts));\n this.registerHandler('remove', (opts) => this.onRemove(opts));\n this.registerHandler('addPriorityJob', (opts) => this.onAddPriorityJob(opts));\n this.registerHandler('removePriorityJob', (opts) => this.onRemovePriorityJob(opts));\n this.registerHandler('pingWorkerId', (opts) => this.onPingWorkerId(opts));\n }\n\n async start() {\n const workerId = this.worker.supervisor.workerId || this.worker.supervisor.workerOpaqueId;\n const identityAddress = this.eventSubscriber.eventRouterConnection.identity.address;\n this.identityAddress = identityAddress;\n this.workerId = workerId;\n this.eventIntercepts = {}\n this.eventIntercepts['schedmsg::broadcast'] = this.onMessage.bind(this);\n this.eventIntercepts['schedmsg::command'] = this.onMessage.bind(this);\n\n await this.eventSubscriber.subscribe('schedmsg::broadcast', {\n filter: {\n $or: [\n {target: identityAddress},\n {target: workerId},\n {broadcast:true},\n ],\n },\n });\n\n await this.eventSubscriber.subscribe('schedmsg::command', {\n filter: {\n $or: [\n {target: identityAddress},\n {target: workerId},\n {broadcast: true},\n ],\n },\n });\n }\n\n async stop() {\n await this.eventSubscriber.close();\n }\n\n registerHandler(command, callback) {\n if (this.listenerCount(command) > 0) {\n throw new Error(`There is a handler already registered for command '${command}'`);\n }\n\n this.on(command, callback);\n }\n\n onMessage(msg) {\n const { command, payload } = msg;\n\n if (this.listenerCount(command) > 0) {\n this.emit(command, payload);\n } else {\n console.warn(`No SchedMsg handler registered for command '${command}'`);\n }\n }\n\n onKill({ temporary }) {\n console.log(\n \"!!!!!!!!!!!!!!!\\n\\n\",\n \"Kill command received from scheduler. Stopping worker...\",\n \"\\n\\n!!!!!!!!!!!!!!!\");\n\n if (this.worker.working) {\n this.worker.stop(true);\n }\n\n if (!temporary) {\n this.worker.constructor.disableWorker();\n }\n }\n\n onRestart() {\n if (this.worker.working) {\n this.worker.stop();\n setTimeout(() => {\n this.worker.start();\n }, Math.random() * 10000 /* stagger workers coming back online */);\n }\n }\n\n onRemove({ jobAddress }) {\n for (let sandbox of this.worker.supervisor.workingSandboxes) {\n if (sandbox.jobAddress === jobAddress) {\n this.worker.supervisor.returnSandbox(sandbox, true);\n }\n }\n }\n\n async onAddPriorityJob({ jobAddress, immediate }) {\n console.log('Received addPriorityJob schedmsg, adding job:', jobAddress);\n\n const supervisor = this.worker.supervisor;\n supervisor.options.jobAddresses.push(jobAddress);\n\n if (immediate) {\n // return all slices that aren't in the priority job list\n await Promise.all(\n supervisor.slices\n .filter((slice) => !supervisor.options.jobAddresses.includes(slice.jobAddress))\n .map((slice) => supervisor.returnSlice(slice))\n ).catch((e) => {\n console.error(\"Error while returning slice before handling addPriorityJob schedmsg:\");\n console.error(e);\n });\n\n // stop all current sandboxes that aren't working on priority slices\n for (let sandbox of supervisor.workingSandboxes) {\n if (!supervisor.options.jobAddresses.includes(sandbox.jobAddress)) {\n supervisor.returnSandbox(sandbox, true);\n }\n }\n }\n\n this.eventIntercepts['stop'] = () => {\n supervisor.options.jobAddresses =\n supervisor.options.jobAddresses.filter((Address) => Address !== jobAddress);\n }\n this.eventSubscriber.subscribe('job::optionalEvent', {\n filter: { job: jobAddress , eventName: 'stop' },\n })\n .catch((e) => {\n console.error(\"Failed to subscribe to stop event for addPriorityJob schedmsg:\");\n console.error(e);\n });\n }\n\n async onRemovePriorityJob({ jobAddress, immediate }) {\n console.log('Received removePriorityJob schedmsg, removing job:', jobAddress);\n\n const supervisor = this.worker.supervisor;\n supervisor.options.jobAddresses =\n supervisor.options.jobAddresses.filter((address) => address !== jobAddress);\n\n if (immediate) {\n // return all slices belonging to this job\n await Promise.all(\n supervisor.slices\n .filter((slice) => slice.jobAddress === jobAddress)\n .map((slice) => supervisor.returnSlice(slice))\n ).catch((e) => {\n console.error(\"Error while returning slice for immediately removed job:\");\n console.error(e);\n });\n\n // stop all current sandboxes that are working on this job\n for (let sandbox of supervisor.workingSandboxes) {\n if (sandbox.jobAddress === jobAddress) {\n supervisor.returnSandbox(sandbox, true);\n }\n }\n }\n }\n async onPingWorkerId() {\n const workerIdentity = this.eventSubscriber.eventRouterConnection.identity;\n let thanatosConnection;\n try {\n this.pongAttempts++;\n thanatosConnection = new protocolV4.Connection(dcpConfig.scheduler.services.thanatos.location, workerIdentity);\n await thanatosConnection.send('worker-pong', {\n workerId: this.workerId,\n });\n this.pongAttempts = 0;\n } catch (error) {\n /* If our send rejects, try again up to 3 times to avoid being pruned by Thanatos */\n if (error instanceof DCPError && this.pongAttempts < 3) { \n console.error('Failed to respond to ping from Thanatos. Trying again to avoid culling by Thanatos.')\n this.onPingWorkerId();\n } else {\n throw error;\n }\n } finally {\n thanatosConnection.close();\n }\n }\n}\n\nexports.SchedMsg = SchedMsg;\n\n\n//# sourceURL=webpack://dcp/./src/dcp-client/schedmsg/schedmsg.js?");
4337
+ eval("/**\n * @file /src/schedmsg/schedmsg.js\n * @author Ryan Rossiter, ryan@kingsds.network\n * @date March 2020\n *\n * This is the client logic for subscribing to and handling SchedMsg events.\n * It inherits from PropagatingEventEmitter, and will register default event\n * callbacks for the commands, but they can be overridden by registering\n * additional event callbacks that return 'false'. This will prevent the default\n * behaviour from occuring.\n */\n\nconst { EventSubscriber } = __webpack_require__(/*! dcp/events/event-subscriber */ \"./src/events/event-subscriber.js\");\nconst { PropagatingEventEmitter } = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\");\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\nconst protocolV4 = __webpack_require__(/*! dcp/protocol-v4 */ \"./src/protocol-v4/index.js\");\n\n/** @typedef {import('../worker').Worker} Worker */\n\n/**\n * SchedMsg is the class that will subscribe to receive commands from the scheduler.\n * These commands could be used to reload the worker, or broacast a message.\n * \n * SchedMsg extends the PropagatingEventEmitter.\n * When a command is received from the scheduler, it will be emitted on the Schedsg\n * instance. SchedMsg will register default handlers for these commands, but they can\n * be overridden by listening for the event and returning `false` from callback you provide.\n * @access public\n */\nclass SchedMsg extends PropagatingEventEmitter {\n /**\n * This command instructs the worker to immediately stop working, and can optionally disable the worker to prevent restarting. The user will need to manually intervene to restart the worker.\n * \n * @event SchedMsg#kill\n * @access public\n * @type {object}\n * @property {boolean} temporary When false, the worker will be disabled.\n *//**\n * This command instructs the worker to restart, e.g. call `worker.stop()` then `worker.start()`.\n * \n * @event SchedMsg#restart\n * @access public\n *//**\n * This command instructs the worker to stop working on a specific job.\n * \n * @event SchedMsg#remove\n * @access public\n * @type {object}\n * @property {string} jobAddress The address of the job to stop working on.\n *//**\n * This command is an announcement from the scheduler, the provided message should be displayed to the user (modal on web, console on node).\n * \n * @event SchedMsg#announce\n * @access public\n * @type {object}\n * @property {string} message The message to be displayed to the user.\n *//**\n * This command instructs the worker to \"hard\" reload, in the browser this will trigger a page refresh and in node it will exit the process.\n * \n * @event SchedMsg#reload\n * @access public\n *//**\n * This *web-only* command will open a new webpage to the provided URL.\n * \n * @event SchedMsg#openPopup\n * @access public\n * @type {object}\n * @property {string} href The URL to open the new page to.\n */\n\n /**\n * @constructor\n * @param {Worker} worker \n */\n constructor(worker) {\n super('SchedMsg');\n\n this.eventSubscriber = new EventSubscriber(this);\n \n this.worker = worker;\n this.handlers = {};\n this.pongAttempts = 0;\n \n this.registerHandler('kill', (opts) => this.onKill(opts));\n this.registerHandler('restart', (opts) => this.onRestart(opts));\n this.registerHandler('remove', (opts) => this.onRemove(opts));\n this.registerHandler('addPriorityJob', (opts) => this.onAddPriorityJob(opts));\n this.registerHandler('removePriorityJob', (opts) => this.onRemovePriorityJob(opts));\n this.registerHandler('pingWorkerId', (opts) => this.onPingWorkerId(opts));\n }\n\n async start() {\n const workerId = this.worker.supervisor.workerId || this.worker.supervisor.workerOpaqueId;\n let eventRouterIdentity = this.eventSubscriber.eventRouterConnection.identity;\n if (!eventRouterIdentity) {\n await this.eventSubscriber.eventRouterConnection.identityPromise;\n eventRouterIdentity = this.eventSubscriber.eventRouterConnection.identity;\n //console.warn('SchedMsg: awaiting eventRouterIdentity', eventRouterIdentity.address);\n }\n const identityAddress = eventRouterIdentity.address;\n this.identityAddress = identityAddress;\n this.workerId = workerId;\n this.eventIntercepts = {}\n this.eventIntercepts['schedmsg::broadcast'] = this.onMessage.bind(this);\n this.eventIntercepts['schedmsg::command'] = this.onMessage.bind(this);\n\n await this.eventSubscriber.subscribe('schedmsg::broadcast', {\n filter: {\n $or: [\n {target: identityAddress},\n {target: workerId},\n {broadcast:true},\n ],\n },\n });\n\n await this.eventSubscriber.subscribe('schedmsg::command', {\n filter: {\n $or: [\n {target: identityAddress},\n {target: workerId},\n {broadcast: true},\n ],\n },\n });\n }\n\n async stop() {\n await this.eventSubscriber.close();\n }\n\n registerHandler(command, callback) {\n if (this.listenerCount(command) > 0) {\n throw new Error(`There is a handler already registered for command '${command}'`);\n }\n\n this.on(command, callback);\n }\n\n onMessage(msg) {\n const { command, payload } = msg;\n\n if (this.listenerCount(command) > 0) {\n this.emit(command, payload);\n } else {\n console.warn(`No SchedMsg handler registered for command '${command}'`);\n }\n }\n\n onKill({ temporary }) {\n console.log(\n \"!!!!!!!!!!!!!!!\\n\\n\",\n \"Kill command received from scheduler. Stopping worker...\",\n \"\\n\\n!!!!!!!!!!!!!!!\");\n\n if (this.worker.working) {\n this.worker.stop(true);\n }\n\n if (!temporary) {\n this.worker.constructor.disableWorker();\n }\n }\n\n onRestart() {\n if (this.worker.working) {\n this.worker.stop();\n setTimeout(() => {\n this.worker.start();\n }, Math.random() * 10000 /* stagger workers coming back online */);\n }\n }\n\n onRemove({ jobAddress }) {\n for (let sandbox of this.worker.supervisor.workingSandboxes) {\n if (sandbox.jobAddress === jobAddress) {\n this.worker.supervisor.returnSandbox(sandbox, true);\n }\n }\n }\n\n async onAddPriorityJob({ jobAddress, immediate }) {\n console.log('Received addPriorityJob schedmsg, adding job:', jobAddress);\n\n const supervisor = this.worker.supervisor;\n supervisor.options.jobAddresses.push(jobAddress);\n\n if (immediate) {\n // return all slices that aren't in the priority job list\n await Promise.all(\n supervisor.slices\n .filter((slice) => !supervisor.options.jobAddresses.includes(slice.jobAddress))\n .map((slice) => supervisor.returnSlice(slice))\n ).catch((e) => {\n console.error(\"Error while returning slice before handling addPriorityJob schedmsg:\");\n console.error(e);\n });\n\n // stop all current sandboxes that aren't working on priority slices\n for (let sandbox of supervisor.workingSandboxes) {\n if (!supervisor.options.jobAddresses.includes(sandbox.jobAddress)) {\n supervisor.returnSandbox(sandbox, true);\n }\n }\n }\n\n this.eventIntercepts['stop'] = () => {\n supervisor.options.jobAddresses =\n supervisor.options.jobAddresses.filter((Address) => Address !== jobAddress);\n }\n this.eventSubscriber.subscribe('job::optionalEvent', {\n filter: { job: jobAddress , eventName: 'stop' },\n })\n .catch((e) => {\n console.error(\"Failed to subscribe to stop event for addPriorityJob schedmsg:\");\n console.error(e);\n });\n }\n\n async onRemovePriorityJob({ jobAddress, immediate }) {\n console.log('Received removePriorityJob schedmsg, removing job:', jobAddress);\n\n const supervisor = this.worker.supervisor;\n supervisor.options.jobAddresses =\n supervisor.options.jobAddresses.filter((address) => address !== jobAddress);\n\n if (immediate) {\n // return all slices belonging to this job\n await Promise.all(\n supervisor.slices\n .filter((slice) => slice.jobAddress === jobAddress)\n .map((slice) => supervisor.returnSlice(slice))\n ).catch((e) => {\n console.error(\"Error while returning slice for immediately removed job:\");\n console.error(e);\n });\n\n // stop all current sandboxes that are working on this job\n for (let sandbox of supervisor.workingSandboxes) {\n if (sandbox.jobAddress === jobAddress) {\n supervisor.returnSandbox(sandbox, true);\n }\n }\n }\n }\n async onPingWorkerId() {\n const workerIdentity = this.eventSubscriber.eventRouterConnection.identity;\n let thanatosConnection;\n try {\n this.pongAttempts++;\n thanatosConnection = new protocolV4.Connection(dcpConfig.scheduler.services.thanatos.location, workerIdentity);\n await thanatosConnection.send('worker-pong', {\n workerId: this.workerId,\n });\n this.pongAttempts = 0;\n } catch (error) {\n /* If our send rejects, try again up to 3 times to avoid being pruned by Thanatos */\n if (error instanceof DCPError && this.pongAttempts < 3) { \n console.error('Failed to respond to ping from Thanatos. Trying again to avoid culling by Thanatos.')\n this.onPingWorkerId();\n } else {\n throw error;\n }\n } finally {\n thanatosConnection.close();\n }\n }\n}\n\nexports.SchedMsg = SchedMsg;\n\n\n//# sourceURL=webpack://dcp/./src/dcp-client/schedmsg/schedmsg.js?");
4283
4338
 
4284
4339
  /***/ }),
4285
4340
 
@@ -4413,7 +4468,8 @@ eval("/**\n * @file node-localExec.js Node-specific support for cre
4413
4468
  \****************************************/
4414
4469
  /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
4415
4470
 
4416
- eval("/**\n * @file This module implements the Worker API, used to create workers for earning DCCs.\n * @author Ryan Rossiter <ryan@kingsds.network>\n * @date May 2020\n * \n * @module dcp/worker\n * @access public\n */\n\nconst { SchedMsg } = __webpack_require__(/*! dcp/dcp-client/schedmsg */ \"./src/dcp-client/schedmsg/index.js\");\nconst { Supervisor } = __webpack_require__(/*! ./supervisor */ \"./src/dcp-client/worker/supervisor.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\");\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 {string|Keystore} [paymentAddress] - Address to deposit earned funds into\n * @property {Keystore} [identityKeystore] - 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} [maxWorkingSandboxes] - Max number of concurrently working sandboxes\n * @property {number} [watchdogInterval] - Number of MS inbetween watchdog cycles, defaults to dcpConfig tuning param\n * @property {any} [computeGroups]\n * @property {object} [minimumWage]\n * @property {string[]} [allowedOrigins] - origins for fetching data URIs that are allowed\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 /** @type {Supervisor} */\n this.supervisor = new Supervisor(this, supervisorOptions);\n /**\n * @type {SchedMsg}\n * @access public\n */\n this.schedMsg = new SchedMsg(this);\n\n this.supervisor.on('fetchingTask', () => this.emit('fetchStart'));\n this.supervisor.on('fetchedTask', (fetchedSlicesCount) => this.emit('fetch', fetchedSlicesCount));\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\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\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?");
4471
+ "use strict";
4472
+ 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));\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\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?");
4417
4473
 
4418
4474
  /***/ }),
4419
4475
 
@@ -4424,7 +4480,7 @@ eval("/**\n * @file This module implements the Worker API, used to create worker
4424
4480
  /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
4425
4481
 
4426
4482
  "use strict";
4427
- eval("// NOTE - need timeout/postmessage function\n/**\n * @file dcp-client/worker/sandbox.js\n *\n * A sandbox that when constructed and assigned can do work for\n * a distributed slice. A sandbox runs for a single slice at a time.\n *\n * Usage:\n * let sandbox = new Sandbox()\n * await sandbox.start()\n * let results = await sandbox.run(slice)\n *\n * Debug flags:\n * Sandbox.debugWork = true // - turns off 30 second timeout to let user debug sandbox innards more easily\n * Sandbox.debugState = true // - logs all state transitions for this sandbox\n * Sandbox.debugEvents = true // - logs all events received from the sandbox\n *\n * @author Matthew Palma, mpalma@kingsds.network\n * Ryan Rossiter, ryan@kingsds.network\n * @date May 2019\n * @module sandbox\n */\n/* global dcpConfig */\n// @ts-check\n\n\nconst debugging = (__webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope)('worker');\nconst { assert, assertEq3 } = __webpack_require__(/*! dcp/common/dcp-assert */ \"./src/common/dcp-assert.js\");\nconst DCP_ENV = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\nconst { rehydrateRange } = __webpack_require__(/*! dcp/dcp-client/range-object */ \"./src/dcp-client/range-object.js\");\nconst kvin = __webpack_require__(/*! kvin */ \"./node_modules/kvin/kvin.js\");\nconst scopedKvin = new kvin.KVIN({Object: ({}).constructor,\n Array: ([]).constructor, \n Function: (()=>{}).constructor});\n\nlet timeDilation = 1;\nif (DCP_ENV.platform === 'nodejs') {\n const { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\n /** Make timers 10x slower when running in niim */\n timeDilation = (requireNative('module')._cache.niim instanceof requireNative('module').Module) ? 10 : 1;\n}\n\n/**\n * Wraps console.debug to emulate debug module prefixing messages on npm.\n * @param {...any} args\n */\nconst debug = (...args) => {\n if (debugging()) {\n console.debug('dcp-client:worker:sandbox', ...args);\n }\n};\n\nconst nanoid = (__webpack_require__(/*! nanoid */ \"./node_modules/nanoid/index.browser.js\").nanoid);\n\nconst { EventEmitter } = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\");\nconst { fetchURI, encodeDataURI } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\n\n// Sandbox states\nconst UNREADY = 'UNREADY' // No Sandbox (web worker, saworker, etc) has been constructed yet\nconst READYING = 'READYING' // Sandbox is being constructed and environment (bravojs, env) is being set up\nconst READY_FOR_ASSIGN = 'READY_FOR_ASSIGN' // Sandbox is ready to be assigned\nconst ASSIGNING = 'ASSIGNING' // Sandbox is running through assigning steps\nconst ASSIGNED = 'ASSIGNED' // Sandbox is assigned but not working\nconst WORKING = 'WORKING' // Sandbox is working\nconst TERMINATED = 'TERMINATED'\nconst EVAL_RESULT_PREFIX = 'evalResult::';\n\nclass SandboxError extends Error {}\nclass NoProgressError extends SandboxError { constructor(...args) { super(...args); this.errorCode = 'ENOPROGRESS'; } }\nclass SliceTooSlowError extends SandboxError { constructor(...args) { super(...args); this.errorCode = 'ESLICETOOSLOW'; } }\nclass UncaughtExceptionError extends SandboxError { constructor(...args) { super(...args); this.errorCode = 'EUNCAUGHT'; } }\nclass RemoteFetchError extends SandboxError { constructor(...args) { super(...args); this.errorCode = 'EFETCH'; }}\n\n/** @typedef {import('dcp/common/dcp-events').EventEmitter} EventEmitter */\n/** @typedef {import('./slice').Slice} Slice */\n/** @typedef {import('./supervisor-cache').SupervisorCache} SupervisorCache */\n/** @typedef {*} opaqueId */\n\n/**\n * @access public\n * @typedef {object} SandboxOptions\n * @constructor {function} [SandboxConstructor]\n * @property {boolean} [ignoreNoProgress] - When true, the sandbox will not be stopped for not calling progress\n */\n\nclass Sandbox extends EventEmitter {\n /**\n * A Sandbox (i.e. a worker sandbox) which executes distributed slices.\n *\n * @constructor\n * @param {SupervisorCache} cache\n * @param {SandboxOptions} options\n */\n constructor (cache, options, origins) {\n super('Sandbox');\n /** @type {SupervisorCache} */\n this.supervisorCache = cache;\n /** @type {SandboxOptions} */\n this.options = {\n ignoreNoProgress: false,\n ...options,\n SandboxConstructor: options.SandboxConstructor ||\n (__webpack_require__(/*! ./evaluators */ \"./src/dcp-client/worker/evaluators/index.js\").BrowserEvaluator),\n }\n this.allowedOrigins = origins;\n\n /** @type {opaqueId} */\n this.jobAddress = null;\n /** @type {object} */\n this.evaluatorHandle = null;\n /** @type {object} */\n this.capabilities = null;\n /** @type {EventEmitter} */\n this.ee = new EventEmitter('SandboxInternal')\n\n /** @type {string} */\n this._state = UNREADY;\n /** @type {boolean} */\n this.allocated = false;\n /** @type {number?} */\n this.progress = 100;\n /** @type {object} */\n this.progressReports = null;\n /** @type {object} */\n this.progressTimeout = null;\n /** @type {object} */\n this.sliceTimeout = null;\n /** @type {object} */\n this.rejectionData = null;\n /** @type {Slice} */\n this.slice = null;\n\n /** @type {number?} */\n this.started = null;\n /** @type {number?} */\n this.sliceStartTime = null;\n /** @type {boolean} */\n this.requiresGPU = false;\n /** @type {string|URL} */\n this.packageURL = dcpConfig.packageManager.location\n /** @type {number?} */\n this.id = Sandbox.getNewId();\n\n this.ringMessageHandlers = [\n this.handleRing0Message,\n this.handleRing1Message,\n this.handleRing2Message,\n this.handleRing3Message,\n ];\n\n this.resetSliceTimeReport();\n }\n\n get identifier() {\n if (this.allocated)\n return `${this.id}.${this.jobAddress}.${this.state}.${this.allocated}`;\n return `${this.id}.${this.jobAddress}.${this.state}`;\n }\n\n static getNewId() {\n return Sandbox.idCounter++;\n }\n\n get state () {\n return this._state\n }\n\n set state (value) {\n if (Sandbox.debugState) {\n console.debug(`sandbox - changing state of ${this.id}... ${this._state} -> ${value}`)\n }\n\n if (this.state === TERMINATED && value !== TERMINATED) {\n // For safety!\n throw new Error(`Sandbox set state violation, attepted to change state from ${this.state} to ${value}`);\n }\n\n this._state = value;\n }\n\n get isReadyForAssign () {\n return this.state === READY_FOR_ASSIGN;\n }\n\n get isAssigned () {\n return this.state === ASSIGNED;\n }\n\n get isWorking () {\n return this.state === WORKING;\n }\n\n get isTerminated () {\n return this.state === TERMINATED;\n }\n\n changeWorkingToAssigned () {\n if (this.isWorking)\n this.state = ASSIGNED;\n }\n\n setIsAssigning () {\n this.state = ASSIGNING;\n }\n\n /**\n * Readies the sandbox. This will result in the sandbox being ready and not assigned,\n * it will need to be assigned with a job before it is able to do work.\n *\n * @todo maybe preload specific modules or let the cache pass in what modules to load?\n * @throws on failure to ready\n */\n async start(delay = 0) {\n this.started = Date.now();\n this.state = READYING;\n\n if (delay > 0) await new Promise((resolve) => setTimeout(resolve, delay * timeDilation));\n\n try {\n // RING 0\n this.evaluatorHandle = new this.options.SandboxConstructor({\n name: `DCP Sandbox #${this.id}`,\n });\n this.evaluatorHandle.onerror = this.onerror.bind(this);\n\n const messageHandler = this.onmessage.bind(this);\n this.evaluatorHandle.onmessage = function onmessage(event)\n {\n let data;\n if (event.data.serialized)\n {\n data = kvin.parse(event.data.message);\n }\n else\n {\n data = kvin.unmarshal(event.data);\n }\n messageHandler({ data });\n }\n\n const evaluatorPostMessage = this.evaluatorHandle.postMessage.bind(this.evaluatorHandle);\n this.evaluatorHandle.postMessage = function postMessage(message)\n {\n evaluatorPostMessage(scopedKvin.marshal(message));\n }\n\n const ceci = this;\n this.evaluatorHandle.addEventListener('end', () => ceci.terminate(true));\n\n // Now in RING 1\n\n // Now in RING 2\n await this.describe();\n this.state = READY_FOR_ASSIGN;\n this.emit('ready', this);\n } catch (error) {\n console.warn('Failed to start the sandbox -', error.message);\n this.terminate(false);\n throw error;\n }\n }\n\n /**\n * This will assign the sandbox with a job, loading its sandbox code\n * into the sandbox.\n *\n * @param {string} jobAddress The address of the job to assign to\n * @throws on initialization failure\n */\n async assign(jobAddress) {\n this.jobAddress = jobAddress;\n this.job = await this.supervisorCache.fetchJob(jobAddress, this.allowedOrigins);\n\n assertEq3(this.job.address, jobAddress);\n assert(typeof this.job === 'object');\n assert(typeof this.job.requirements === 'object');\n assert(Array.isArray(this.job.dependencies));\n assert(Array.isArray(this.job.requirePath));\n\n // Extract public data from job, with defaults\n this.public = Object.assign({\n name: `Anonymous Job ${this.job.address.slice(0, 6)}`,\n description: 'Discreetly helping make the world smarter.',\n link: 'https://distributed.computer/about',\n }, this.job.public);\n\n // Future: We may want other filename tags for appliances // RR Nov 2019\n\n // Important: The order of applying requirements before loading the sandbox code\n // is important for modules and sandbox code to set globals over the whitelist.\n await this.applySandboxRequirements(this.job.requirements);\n await this.assignEvaluator();\n this.state = ASSIGNED;\n }\n\n async assignEvaluator() {\n debug('Begin assigning job to evaluator');\n const ceci = this;\n\n return new Promise(function sandbox$$assignEvaluatorPromise(resolve, reject) {\n const message = {\n request: 'assign',\n job: ceci.job,\n sandboxConfig: dcpConfig.worker.sandbox,\n };\n\n /* note - onFailListener used for removal. This is necessary due to a bug in ee.once. /wg Feb 2022 */\n \n const onSuccess = (event) => {\n // eslint-disable-next-line no-use-before-define\n ceci.ee.removeListener('reject', onFailListener);\n ceci.emit('assigned', event.jobAddress);\n debug('Job assigned to evaluator');\n resolve();\n };\n\n const onFail = (error) => {\n // eslint-disable-next-line no-use-before-define\n ceci.ee.removeListener('assigned', onSuccessListener);\n reject(error);\n };\n\n const onSuccessListener = ceci.ee.once('assigned', onSuccess);\n const onFailListener = ceci.ee.once('reject', onFail);\n ceci.evaluatorHandle.postMessage(message);\n });\n }\n\n /**\n * Evaluates a string inside the sandbox.\n *\n * @param {string} code - the code to evaluate in the sandbox\n * @param {string} filename - the name of the 'file' to help with debugging,\n * no longer working though?\n * @returns {Promise} - resolves with eval result on success, rejects\n * otherwise\n */\n eval(code, filename) {\n var ceci = this;\n \n return new Promise(function sandbox$$eval$Promise(resolve, reject) {\n let msgId = nanoid();\n let msg = {\n request: 'eval',\n data: code,\n filename,\n msgId, \n }\n\n const eventId = EVAL_RESULT_PREFIX + msgId;\n\n let onSuccess = (event) => {\n ceci.ee.removeListener('reject', onFailListener)\n resolve(event)\n }\n\n let onFail = (event) => {\n ceci.ee.removeListener(eventId, onSuccessListener)\n reject(event)\n }\n\n let onSuccessListener = ceci.ee.once(eventId, onSuccess);\n let onFailListener = ceci.ee.once('reject', onFail)\n\n ceci.evaluatorHandle.postMessage(msg)\n })\n }\n\n /**\n * Resets the state of the bootstrap, without resetting the sandbox function if assigned.\n * Mostly used to reset the progress status before reusing a sandbox on another slice.\n * Must be called after @start.\n *\n * @returns {Promise} - resolves with result on success, rejects otherwise\n */\n resetSandboxState () {\n var ceci = this;\n\n return new Promise(function sandbox$resetSandboxStatePromise(resolve, reject) {\n let successCb, failTimeout;\n let msg = {\n request: 'resetState',\n };\n\n successCb = ceci.ee.once('resetStateDone', function sandbox$resetSandboxState$success () {\n if (failTimeout === false)\n return; /* already rejected */\n clearTimeout(failTimeout);\n failTimeout = false;\n resolve();\n });\n\n failTimeout = setTimeout(function sandbox$resetSandboxState$fail() {\n if (failTimeout === false)\n return; /* already resolved */\n \n ceci.ee.removeListener('resetStateDone', successCb);\n ceci.terminate(false);\n failTimeout = false;\n\n reject(new Error('resetState never received resetStateDone event from sandbox'));\n }, 3000 * timeDilation); /* XXXwg need tuneable */\n\n assert(ceci.evaluatorHandle); // It is possible that ceci.terminate nulls out evaluatorHandle before getting here.\n ceci.evaluatorHandle.postMessage(msg);\n });\n }\n\n /**\n * Clear all timers that are set inside the sandbox (evaluator) environment.\n *\n * @returns {Promise} - resolves with result on success, rejects otherwise\n */\n clearSandboxTimers() {\n var ceci = this;\n \n return new Promise(function sandbox$clearSandboxTimersPromise(resolve, reject) {\n let successCb, failTimeout;\n let msg = {\n request: 'clearTimers',\n };\n\n successCb = ceci.ee.once('clearTimersDone', function sandbox$clearSandboxTimers$success() {\n if (failTimeout === false)\n return; /* already rejected */\n clearTimeout(failTimeout);\n failTimeout = false;\n resolve();\n });\n\n failTimeout = setTimeout(function sanbox$clearSandboxTimers$fail() {\n if (failTimeout === false)\n return; /* already resolved */\n \n ceci.ee.removeListener('clearTimersDone', successCb);\n ceci.terminate(false);\n failTimeout = false;\n \n reject(new Error('clearTimers never received clearTimersDone event from sandbox'));\n }, 3000 * timeDilation); /* XXXwg need tuneable */\n\n if (ceci.evaluatorHandle) // Sometimes ceci.terminate nulls out evaluatorHandle before getting here.\n ceci.evaluatorHandle.postMessage(msg);\n });\n }\n\n /**\n * Sends a post message to describe its capabilities.\n *\n * Side effect: Sets the capabilities property of the current sandbox.\n *\n * @returns {Promise} Resolves with the sandbox's capabilities. Rejects with\n * an error saying a response was not received.\n * @memberof Sandbox\n */\n describe() {\n debug('Beginning to describe evaluator');\n var ceci = this;\n \n return new Promise(function sandbox$describePromise(resolve, reject) {\n if (ceci.evaluatorHandle === null) {\n return reject(new Error('Evaluator has not been initialized.'));\n }\n\n /**\n * Opted to create a flag for the describe response being received so that\n * we don't have to *hoist* the timeout's id to clear it in the response\n * handler.\n */\n let didReceiveDescribeResponse = false;\n const describeResponseHandler = ceci.ee.once('describe', (data) => {\n didReceiveDescribeResponse = true;\n const { capabilities } = data;\n if (typeof capabilities === 'undefined') {\n reject(\n new Error('Did not receive capabilities from describe response.'),\n );\n }\n ceci.capabilities = capabilities;\n\n // Currently only used in tests. May use the event in the future.\n ceci.emit('described', capabilities);\n debug('Evaluator has been described');\n resolve(capabilities);\n });\n const describeResponseFailedHandler = () => {\n if (!didReceiveDescribeResponse) {\n ceci.ee.removeListener('describe', describeResponseHandler);\n ceci.terminate(false);\n reject(\n new Error(\n 'Describe message timed-out. No describe response was received from the describe command.',\n ),\n );\n }\n };\n\n const message = {\n request: 'describe',\n };\n\n // Arbitrarily set the waiting time.\n setTimeout(describeResponseFailedHandler, 6000 * timeDilation); /* XXXwg need tuneable */\n assert(ceci.evaluatorHandle); // It is possible that ceci.terminate nulls out evaluatorHandle before getting here.\n ceci.evaluatorHandle.postMessage(message);\n });\n }\n\n /**\n * Passes the job's requirements object into the sandbox so that the global\n * access lists can be updated accordingly.\n *\n * e.g. disallow access to OffscreenCanvas without\n * environment.offscreenCanvas=true present.\n *\n * Must be called after @start.\n *\n * @returns {Promise} - resolves with result on success, rejects otherwise\n */\n applySandboxRequirements(requirements) {\n var ceci = this;\n \n return new Promise(function sandbox$applySandboxRequirementsPromise(resolve, reject) {\n const message = {\n requirements,\n request: 'applyRequirements',\n };\n let wereRequirementsApplied = false;\n\n const successCb = ceci.ee.once(\n 'applyRequirementsDone',\n function sandbox$applyRequirements$success() {\n wereRequirementsApplied = true;\n resolve();\n },\n );\n\n assert(typeof message.requirements === 'object');\n ceci.evaluatorHandle.postMessage(message);\n\n setTimeout(function sandbox$finishApplySandboxRequirements() {\n if (!wereRequirementsApplied) {\n ceci.ee.removeListener('applyRequirementsDone', successCb);\n ceci.terminate(false);\n reject(\n new Error(\n 'applyRequirements never received applyRequirementsDone response from sandbox',\n ),\n );\n }\n }, 3000 * timeDilation); /* XXXwg needs tunable */\n });\n }\n\n /**\n * Executes a slice received from the supervisor.\n * Must be called after @start.\n *\n * @param {Slice} slice - bare minimum data required for the job/job code to be executed on\n * @param {number} [delay = 0] the delay that this method should wait before beginning work, used to avoid starting all sandboxes at once\n *\n * @returns {Promise} - resolves with result on success, rejects otherwise\n */\n\n async work (slice, delay = 0) {\n var ceci = this;\n\n if (!ceci.isAssigned) {\n throw new Error(\"Sandbox.run: Sandbox is not ready to work, state=\" + ceci.state);\n }\n\n ceci.state = WORKING;\n ceci.slice = slice;\n assert(slice);\n\n // cf. DCP-1720\n this.resetSliceTimeReport();\n \n // Now wait for the delay if provided, prevents many sandboxes starting at once from crashing the supervisor\n if (delay > 0) await new Promise(resolve => setTimeout(resolve, (delay + 1) * timeDilation));\n if (!ceci.isWorking) return; // sandbox.terminate could have been called during the delay timeout\n\n // Prepare the sandbox to begin work\n // will be replaced by `assign` message that should be called before emitting a `work` message\n if (ceci.jobAddress !== slice.jobAddress) {\n throw new Error(`Sandbox.run: Sandbox is already assigned and jobAddress doesn't match previous (${ceci.jobAddress} !== ${slice.jobAddress})`);\n }\n\n let sliceHnd = { job: ceci.public, sandbox: ceci };\n await ceci.resetSandboxState();\n if (!ceci.slice) {\n console.error(`Slice for job ${ceci.jobAddress} vanished during work initialization - aborting`);\n return;\n }\n\n let inputDatum;\n let dataError = false;\n try {\n if (ceci.slice.datumUri)\n inputDatum = await fetchURI(\n ceci.slice.datumUri,\n this.allowedOrigins,\n dcpConfig.worker.allowOrigins.fetchData,\n );\n else {\n let { mro } = await ceci.supervisorCache.fetchJob(\n ceci.jobAddress,\n this.allowedOrigins.concat(dcpConfig.worker.allowOrigins.fetchData)\n );\n const ro = rehydrateRange(mro);\n // -1 to prevent an OBOE since slice numbers start at 1.\n inputDatum = ro[ceci.slice.sliceNumber - 1];\n }\n } catch (err) {\n dataError = err;\n if(err.code === 'EFETCH')\n dataError.errorCode = 'EFETCH'\n else\n dataError.errorCode = 'EUNCAUGHTERROR'\n ceci.emit('workEmit', {\n eventName: 'error',\n payload: {\n message: dataError.message,\n stack:dataError.stack,\n name: ceci.public.name\n }\n });\n }\n\n debugging('sandbox') && debug(`Fetched datum: ${inputDatum}`);\n\n if (!ceci.slice) {\n console.error(`Slice for job ${ceci.jobAddress} vanished after data fetch - aborting`);\n return;\n }\n\n ceci.resetProgressTimeout();\n ceci.resetSliceTimeout();\n\n return new Promise(function sandbox$$workPromise(resolve, reject) {\n let onSuccess, onFail\n\n onSuccess = ceci.ee.once('resolve', function sandbox$$work$success (event) {\n ceci.ee.removeListener('reject', onFail)\n resolve(event)\n }.bind(ceci))\n\n onFail = ceci.ee.once('reject', function sandbox$$work$fail (err) {\n ceci.ee.removeListener('resolve', onSuccess)\n reject(err)\n }.bind(ceci))\n\n ceci.sliceStartTime = Date.now();\n ceci.progress = null;\n ceci.progressReports = {\n last: undefined,\n lastDeterministic: undefined,\n };\n\n ceci.resetProgressTimeout();\n ceci.resetSliceTimeout();\n ceci.emit('start', sliceHnd);\n \n if(dataError){\n ceci.ee.removeListener('resolve', onSuccess);\n ceci.ee.removeListener('reject', onFail);\n setTimeout(() => reject(dataError), 0)\n\n } else {\n ceci.evaluatorHandle.postMessage({\n request: 'main',\n data: inputDatum,\n })\n }\n })\n .then(async function sandbox$$work$then(event) {\n // Ceci is the complete callback when the slice completes\n // prevent any hanging timers from being fired\n await ceci.clearSandboxTimers();\n\n // TODO: Should sliceHnd just be replaced with ceci.public?\n ceci.emit('slice', sliceHnd); /** @todo: decide which event is right */\n ceci.emit('sliceFinish', event);\n ceci.emit('complete', event);\n\n ceci.changeWorkingToAssigned();\n ceci.slice = false;\n return event;\n })\n .catch((err) => { \n if (err.name === 'EWORKREJECT') {\n this.rejectionData = err;\n ceci.evaluatorHandle.postMessage({ request: 'resetAndGetCPUTime' })\n } else { // sandbox termination for rejected work happens in Supervisor.handleRejectedWork\n // Ceci is the reject callback for when the slice throws an error\n ceci.terminate(false);\n }\n\n ceci.emit('error', err, 'slice');\n\n if (err instanceof NoProgressError) {\n ceci.emit('workEmit', {\n eventName: 'noProgress',\n payload: {\n timestamp: Date.now() - ceci.sliceStartTime,\n data: ceci.slice.datumUri,\n progressReports: ceci.progressReports,\n }\n });\n }\n throw err;\n })\n .finally(function sandbox$$work$finally() {\n ceci.emit('end', sliceHnd);\n });\n }\n\n resetProgressTimeout() {\n if (this.progressTimeout) {\n clearTimeout(this.progressTimeout);\n this.progressTimeout = null;\n }\n\n this.progressTimeout = setTimeout(() => {\n if (this.options.ignoreNoProgress) {\n return console.warn(\"ENOPROGRESS silenced by localExec: In a remote worker, this slice would be stopped for not calling progress frequently enough.\");\n }\n\n this.ee.emit('reject', new NoProgressError(`No progress event was received in the last ${dcpConfig.worker.sandbox.progressTimeout / 1000} seconds.`));\n }, +dcpConfig.worker.sandbox.progressTimeout * timeDilation);\n }\n\n resetSliceTimeout() {\n if (this.sliceTimeout) clearTimeout(this.sliceTimeout);\n\n this.sliceTimeout = setTimeout(() => {\n if (Sandbox.debugWork) return console.warn(\"Sandbox.debugWork: Ignoring slice timeout\");\n\n this.ee.emit('reject', new SliceTooSlowError(`Slice took longer than ${dcpConfig.worker.sandbox.sliceTimeout / 1000} seconds.`));\n }, +dcpConfig.worker.sandbox.sliceTimeout * timeDilation);\n }\n \n async handleRing0Message(data) {\n debugging('event:ring-0') && debug('event:ring-0', data);\n //handling a true ring 0 message\n switch (data.request) {\n case 'sandboxLoaded':\n // emit externally\n this.emit('sandboxLoaded', this)\n break;\n\n case 'scriptLoaded':\n // emit externally\n this.emit('scriptLoaded', data);\n \n if(data.result !== \"success\") {\n this.onerror(data);\n }\n break;\n \n case 'clearTimersDone':\n this.ee.emit(data.request, data);\n break;\n case 'totalCPUTime':\n this.updateTime(data);\n if (this.ee.listenerCount('resolve') > 0) {\n this.completeData.timeReport = this.sliceTimeReport;\n this.ee.emit('resolve', this.completeData);\n delete this.completeData;\n } else {\n this.rejectionData.timeReport = this.sliceTimeReport\n this.emit('rejectedWorkMetrics', this.rejectionData) // If there is no internal listener for 'resolve', the slice was rejected and\n delete this.rejectionData; // we need to send the rejected metrics to the supervisor\n } \n break;\n case 'error':\n // Warning: rejecting here with just event.data.error causes issues\n // where the reject handlers modify the object so it interferes with the\n // workEmit event payload, wrapping in an Error instance copies the values\n let e = new Error(\n data.error.message,\n data.error.fileName,\n data.error.lineNumber);\n e.stack = data.error.stack;\n e.name = data.error.name;\n \n if (this.ee.listenerCount('reject') > 0) {\n this.ee.emit('reject', e);\n } else {\n // This will happen if the error is thrown during initialization\n throw e;\n }\n\n break;\n default:\n let error = new Error('Received unhandled request from sandbox: ' + data.request + '\\n\\t' + JSON.stringify(data));\n console.error(error);\n break; \n }\n }\n\n async handleRing1Message(data) {\n switch (data.request) {\n case 'applyRequirementsDone':\n // emit internally\n this.ee.emit(data.request, data)\n break;\n default:\n let error = new Error('Received unhandled request from sandbox ring 1: ' + data.request + '\\n\\t' + JSON.stringify(data));\n console.error(error)\n break; \n }\n }\n\n async handleRing2Message(data) {\n debugging('event:ring-2') && debug('event:ring-2', data);\n switch (data.request) {\n case 'dependency': {\n let moduleData;\n try {\n moduleData = await this.supervisorCache.fetchModule(data.data);\n } catch (error) {\n /*\n * In the event of an error here, we want to let the client know there was a problem in\n * loading their module. However, there hasn't yet been an actual slice assigned to the sandbox.\n * Therefore, we assign 'slice 0' to the sandbox, a slice that will never exist, and is used\n * purely for this purpose. \n */\n this.slice = {\n jobAddress: this.jobAddress,\n sliceNumber: 0,\n };\n\n const payload = {\n name: error.name,\n timestamp: error.timestamp,\n message: error.message,\n };\n\n this.emit('workEmit', {\n eventName: 'error',\n payload,\n });\n this.ee.emit('reject', error);\n break;\n }\n this.evaluatorHandle.postMessage({\n request: 'moduleGroup',\n data: moduleData,\n id: data.id,\n });\n break;\n }\n case 'error':\n /*\n * Ring 2 error messages will only fire for problems inside of the worker that are separate from\n * the work function. In most cases there are other handlers for situations where 'error' may be emitted\n * such as timeouts if the expected message isn't recieved. Thus, we will output the error, but nothing else.\n */\n console.error(data.error);\n break;\n case 'describe':\n case 'evalResult':\n case 'resetStateDone':\n case 'assigned':\n // emit internally\n this.ee.emit(data.request, data);\n break;\n case 'reject':\n // emit internally\n this.ee.emit(data.request, data.error);\n break;\n default: {\n const error = new Error(\n `Received unhandled request from sandbox ring 2. Data: ${JSON.stringify(\n data,\n null,\n 2,\n )}`,\n );\n console.error(error);\n break;\n }\n }\n }\n\n async handleRing3Message(data) {\n switch (data.request) {\n case 'complete':\n clearTimeout(this.progressTimeout);\n clearTimeout(this.sliceTimeout);\n this.progressTimeout = this.sliceTimeout = null;\n\n if (this.progress === null) {\n if (this.options.ignoreNoProgress) {\n console.warn(\"ENOPROGRESS silenced by localExec: Progress was not called during this slice's execution, in a remote sandbox this would cause the slice to be failed\");\n } else {\n // If a progress update was never received (progress === null) then reject\n this.ee.emit('reject', new NoProgressError('Sandbox never emitted a progress event.'));\n break;\n }\n }\n this.evaluatorHandle.postMessage({ request: 'resetAndGetCPUTime' })\n this.progress = 100;\n this.completeData = data;\n // The timing report and resolve will be emitted when the CPU time is received. \n break;\n case 'progress':\n let { progress, indeterminate, throttledReports, value } = data;\n this.progress = progress;\n const progressReport = {\n timestamp: Date.now() - this.sliceStartTime,\n progress,\n value,\n throttledReports,\n }\n this.progressReports.last = progressReport;\n if (!indeterminate) {\n this.progressReports.lastDeterministic = progressReport;\n }\n\n this.resetProgressTimeout();\n\n this.emit('sliceProgress', data);\n break;\n\n case 'noProgress':\n let { message } = data;\n\n this.ee.emit('reject', new NoProgressError(message));\n break;\n case 'console':\n data.payload.message = scopedKvin.marshal(data.payload.message); \n this.emit('workEmit', {\n eventName: 'console',\n payload: data.payload, \n });\n break;\n\n case 'emitEvent':/* ad-hoc event from the sandbox (work.emit) */\n this.emit('workEmit', {\n eventName: 'custom',\n payload: data.payload\n })\n break;\n case 'measurement':\n this.updateTime(data);\n break;\n case 'sandboxError': /* the sandbox itself has an error condition */\n {\n this.emit('sandboxError', data.error);\n break;\n }\n case 'workError': { /* the work function threw/rejected */\n this.emit('workEmit', {\n eventName: 'error',\n payload: data.error,\n });\n\n // Warning: rejecting here with just .data.error causes issues\n // where the reject handlers modify the object so it interferes with the\n // workEmit payload, wrapping in an Error instance copies the values\n const wrappedError = new UncaughtExceptionError(\n data.error.message,\n data.error.fileName,\n data.error.lineNumber,\n );\n wrappedError.stack = data.error.stack;\n wrappedError.name = data.error.name;\n\n if (this.ee.listenerCount('reject') > 0) {\n this.ee.emit('reject', wrappedError);\n } else {\n // This will happen if the error is thrown during initialization\n throw wrappedError;\n }\n break;\n }\n default:\n let error = new Error('Received unhandled request from sandbox ring 3: ' + data.request + '\\n\\t' + JSON.stringify(data));\n console.error(error)\n break; \n }\n }\n\n /**\n * Handles progress and completion events from sandbox.\n * Unless explicitly returned out of this function will re-emit the event\n * on @this.ee where the name of the event is event.data.request.\n *\n * @param {object} event - event received from the sandbox\n */\n async onmessage(event) {\n debugging('event') && debug('event', event);\n if (Sandbox.debugEvents) {\n console.debug('sandbox - eventDebug:', {\n id: this.id,\n state: this.state,\n event: JSON.stringify(event)\n })\n }\n\n const { data } = event;\n const ringLevel = data.ringSource\n\n // Give the data to a handler depending on ring level\n if (ringLevel === -1) {\n console.error('Message sent directly from raw postMessage. Terminating worker...');\n console.debug(event);\n return this.terminate(true);\n } else {\n const handler = this.ringMessageHandlers[ringLevel];\n if (handler) {\n handler.call(this, data.value);\n } else {\n console.warn(`No handler defined for message from ring ${ringLevel}`);\n console.debug(event);\n }\n }\n }\n\n /**\n * Error handler for the internal sandbox.\n * Currently just logs the errors that the sandbox spits out.\n */\n onerror(event) {\n console.error('Sandbox emitted an error:', event);\n this.terminate(true, true);\n }\n\n /**\n * Clears the timeout and terminates the sandbox and sometimes emits a reject event.\n *\n * @param {boolean} [reject = true] - if true emit reject event\n * @param {boolean} [immediate = false] - passed to terminate, used by standaloneWorker to immediately close the connection\n */\n terminate (reject = true, immediate = false) {\n const oldState = this.state;\n this.state = TERMINATED;\n \n clearTimeout(this.progressTimeout);\n clearTimeout(this.sliceTimeout);\n this.progressTimeout = this.sliceTimeout = null;\n \n if (this.evaluatorHandle && typeof this.evaluatorHandle.terminate === 'function') {\n try {\n this.evaluatorHandle.terminate(immediate);\n this.evaluatorHandle = null;\n } catch (e) {\n console.error(`Error terminating sandbox ${this.id} (${oldState}):`, e);\n } finally {\n this.emit('terminate', this);\n }\n }\n\n if (reject) {\n this.ee.emit('reject', new Error(`Sandbox ${this.id} (${oldState}) was terminated.`));\n }\n\n this.emit('terminated');\n }\n\n /**\n * Attempts to stop the sandbox from doing completing its current\n * set of work without terminating the working.\n */\n stop () {\n throw new Error('Sandbox.stop is not yet implemented.')\n }\n\n /**\n * ringNPostMessage can send a `measurement` request and update these\n * totals.\n */\n updateTime (measurementEvent) {\n ['total', 'CPU', 'webGL'].forEach((key) => {\n if (measurementEvent[key]) this.sliceTimeReport[key] += measurementEvent[key];\n })\n }\n\n resetSliceTimeReport () {\n this.sliceTimeReport = {\n total: 0,\n CPU: 0,\n webGL: 0,\n }\n }\n}\n\nSandbox.idCounter = 1;\nSandbox.debugWork = false;\nSandbox.debugState = false;\nSandbox.debugEvents = false;\n\nexports.Sandbox = Sandbox;\nexports.SandboxError = SandboxError;\nexports.NoProgressError = NoProgressError;\nexports.SliceTooSlowError = SliceTooSlowError;\nexports.UncaughtExceptionError = UncaughtExceptionError;\nexports.RemoteFetchError = RemoteFetchError;\n\n\n//# sourceURL=webpack://dcp/./src/dcp-client/worker/sandbox.js?");
4483
+ eval("// NOTE - need timeout/postmessage function\n/**\n * @file dcp-client/worker/sandbox.js\n *\n * A sandbox that when constructed and assigned can do work for\n * a distributed slice. A sandbox runs for a single slice at a time.\n *\n * Usage:\n * let sandbox = new Sandbox()\n * await sandbox.start()\n * let results = await sandbox.run(slice)\n *\n * Debug flags:\n * Sandbox.debugWork = true // - turns off 30 second timeout to let user debug sandbox innards more easily\n * Sandbox.debugState = true // - logs all state transitions for this sandbox\n * Sandbox.debugEvents = true // - logs all events received from the sandbox\n *\n * @author Matthew Palma, mpalma@kingsds.network\n * Ryan Rossiter, ryan@kingsds.network\n * @date May 2019\n * @module sandbox\n */\n/* global dcpConfig */\n// @ts-check\n\n\nconst debugging = (__webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope)('worker');\nconst { assert, assertEq3 } = __webpack_require__(/*! dcp/common/dcp-assert */ \"./src/common/dcp-assert.js\");\nconst DCP_ENV = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\nconst { rehydrateRange } = __webpack_require__(/*! dcp/dcp-client/range-object */ \"./src/dcp-client/range-object.js\");\nconst kvin = __webpack_require__(/*! kvin */ \"./node_modules/kvin/kvin.js\");\nconst scopedKvin = new kvin.KVIN({Object: ({}).constructor,\n Array: ([]).constructor, \n Function: (()=>{}).constructor});\n\nlet timeDilation = 1;\nif (DCP_ENV.platform === 'nodejs') {\n const { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\n /** Make timers 10x slower when running in niim */\n timeDilation = (requireNative('module')._cache.niim instanceof requireNative('module').Module) ? 10 : 1;\n}\n\n/**\n * Wraps console.debug to emulate debug module prefixing messages on npm.\n * @param {...any} args\n */\nconst debug = (...args) => {\n if (debugging()) {\n console.debug('dcp-client:worker:sandbox', ...args);\n }\n};\n\nconst nanoid = (__webpack_require__(/*! nanoid */ \"./node_modules/nanoid/index.browser.js\").nanoid);\n\nconst { EventEmitter } = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\");\nconst { fetchURI, encodeDataURI } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\n\n// Sandbox states\nconst UNREADY = 'UNREADY' // No Sandbox (web worker, saworker, etc) has been constructed yet\nconst READYING = 'READYING' // Sandbox is being constructed and environment (bravojs, env) is being set up\nconst READY_FOR_ASSIGN = 'READY_FOR_ASSIGN' // Sandbox is ready to be assigned\nconst ASSIGNING = 'ASSIGNING' // Sandbox is running through assigning steps\nconst ASSIGNED = 'ASSIGNED' // Sandbox is assigned but not working\nconst WORKING = 'WORKING' // Sandbox is working\nconst TERMINATED = 'TERMINATED'\nconst EVAL_RESULT_PREFIX = 'evalResult::';\n\nclass SandboxError extends Error {}\nclass NoProgressError extends SandboxError { constructor(...args) { super(...args); this.errorCode = 'ENOPROGRESS'; } }\nclass SliceTooSlowError extends SandboxError { constructor(...args) { super(...args); this.errorCode = 'ESLICETOOSLOW'; } }\nclass UncaughtExceptionError extends SandboxError { constructor(...args) { super(...args); this.errorCode = 'EUNCAUGHT'; } }\nclass RemoteFetchError extends SandboxError { constructor(...args) { super(...args); this.errorCode = 'EFETCH_BAD_ORIGIN'; }}\n\n/** @typedef {import('dcp/common/dcp-events').EventEmitter} EventEmitter */\n/** @typedef {import('./slice').Slice} Slice */\n/** @typedef {import('./supervisor-cache').SupervisorCache} SupervisorCache */\n/** @typedef {*} opaqueId */\n\n/**\n * @access public\n * @typedef {object} SandboxOptions\n * @constructor {function} [SandboxConstructor]\n * @property {boolean} [ignoreNoProgress] - When true, the sandbox will not be stopped for not calling progress\n */\n\nclass Sandbox extends EventEmitter {\n /**\n * A Sandbox (i.e. a worker sandbox) which executes distributed slices.\n *\n * @constructor\n * @param {object} supervisor the instance of Supervisor that owns this sandbox\n * @param {SandboxOptions} options\n */\n constructor (supervisor, options) {\n super('Sandbox');\n this.supervisor = supervisor;\n /** @type {SupervisorCache} */\n this.supervisorCache = supervisor.cache;\n /** @type {SandboxOptions} */\n this.options = {\n ignoreNoProgress: false,\n ...options,\n SandboxConstructor: options.SandboxConstructor ||\n (__webpack_require__(/*! ./evaluators */ \"./src/dcp-client/worker/evaluators/index.js\").BrowserEvaluator),\n }\n\n /** @type {opaqueId} */\n this.jobAddress = null;\n /** @type {object} */\n this.evaluatorHandle = null;\n /** @type {object} */\n this.capabilities = null;\n /** @type {EventEmitter} */\n this.ee = new EventEmitter('SandboxInternal')\n\n /** @type {string} */\n this._state = UNREADY;\n /** @type {boolean} */\n this.allocated = false;\n /** @type {number?} */\n this.progress = 100;\n /** @type {object} */\n this.progressReports = null;\n /** @type {object} */\n this.progressTimeout = null;\n /** @type {object} */\n this.sliceTimeout = null;\n /** @type {object} */\n this.rejectionData = null;\n /** @type {Slice} */\n this.slice = null;\n\n /** @type {number?} */\n this.started = null;\n /** @type {number?} */\n this.sliceStartTime = null;\n /** @type {boolean} */\n this.requiresGPU = false;\n /** @type {string|URL} */\n this.packageURL = dcpConfig.packageManager.location\n /** @type {number?} */\n this.id = Sandbox.getNewId();\n\n this.ringMessageHandlers = [\n this.handleRing0Message,\n this.handleRing1Message,\n this.handleRing2Message,\n this.handleRing3Message,\n ];\n\n this.resetSliceTimeReport();\n }\n\n get identifier() {\n if (this.allocated)\n return `${this.id}.${this.jobAddress}.${this.state}.${this.allocated}`;\n return `${this.id}.${this.jobAddress}.${this.state}`;\n }\n\n static getNewId() {\n return Sandbox.idCounter++;\n }\n\n get state () {\n return this._state\n }\n\n set state (value) {\n if (Sandbox.debugState) {\n console.debug(`sandbox - changing state of ${this.id}... ${this._state} -> ${value}`)\n }\n\n if (this.state === TERMINATED && value !== TERMINATED) {\n // For safety!\n throw new Error(`Sandbox set state violation, attepted to change state from ${this.state} to ${value}`);\n }\n\n this._state = value;\n }\n\n get isReadyForAssign () {\n return this.state === READY_FOR_ASSIGN;\n }\n\n get isAssigned () {\n return this.state === ASSIGNED;\n }\n\n get isWorking () {\n return this.state === WORKING;\n }\n\n get isTerminated () {\n return this.state === TERMINATED;\n }\n\n changeWorkingToAssigned () {\n if (this.isWorking)\n this.state = ASSIGNED;\n }\n\n setIsAssigning () {\n this.state = ASSIGNING;\n }\n\n /**\n * Readies the sandbox. This will result in the sandbox being ready and not assigned,\n * it will need to be assigned with a job before it is able to do work.\n *\n * @todo maybe preload specific modules or let the cache pass in what modules to load?\n * @throws on failure to ready\n */\n async start(delay = 0) {\n this.started = Date.now();\n this.state = READYING;\n\n if (delay > 0) await new Promise((resolve) => setTimeout(resolve, delay * timeDilation));\n\n try {\n // RING 0\n this.evaluatorHandle = new this.options.SandboxConstructor({\n name: `DCP Sandbox #${this.id}`,\n });\n this.evaluatorHandle.onerror = this.onerror.bind(this);\n\n const messageHandler = this.onmessage.bind(this);\n this.evaluatorHandle.onmessage = function onmessage(event)\n {\n let data;\n if (event.data.serialized)\n {\n data = kvin.parse(event.data.message);\n }\n else\n {\n data = kvin.unmarshal(event.data);\n }\n messageHandler({ data });\n }\n\n const evaluatorPostMessage = this.evaluatorHandle.postMessage.bind(this.evaluatorHandle);\n this.evaluatorHandle.postMessage = function postMessage(message)\n {\n evaluatorPostMessage(scopedKvin.marshal(message));\n }\n\n const ceci = this;\n this.evaluatorHandle.addEventListener('end', () => ceci.terminate(true));\n\n // Now in RING 1\n\n // Now in RING 2\n await this.describe();\n this.state = READY_FOR_ASSIGN;\n this.emit('ready', this);\n } catch (error) {\n console.warn('Failed to start the sandbox -', error.message);\n this.terminate(false);\n throw error;\n }\n }\n\n /**\n * This will assign the sandbox with a job, loading its sandbox code\n * into the sandbox.\n *\n * @param {string} jobAddress The address of the job to assign to\n * @throws on initialization failure\n */\n async assign(jobAddress) {\n this.jobAddress = jobAddress;\n this.job = await this.supervisorCache.fetchJob(jobAddress, this.supervisor.makeSafeOriginList.bind(this.supervisor));\n\n assertEq3(this.job.address, jobAddress);\n assert(typeof this.job === 'object');\n assert(typeof this.job.requirements === 'object');\n assert(Array.isArray(this.job.dependencies));\n assert(Array.isArray(this.job.requirePath));\n\n // Extract public data from job, with defaults\n this.public = Object.assign({\n name: `Anonymous Job ${this.job.address.slice(0, 6)}`,\n description: 'Discreetly helping make the world smarter.',\n link: 'https://distributed.computer/about',\n }, this.job.public);\n\n // Future: We may want other filename tags for appliances // RR Nov 2019\n\n // Important: The order of applying requirements before loading the sandbox code\n // is important for modules and sandbox code to set globals over the whitelist.\n await this.applySandboxRequirements(this.job.requirements);\n await this.assignEvaluator();\n this.state = ASSIGNED;\n }\n\n async assignEvaluator() {\n debug('Begin assigning job to evaluator');\n const ceci = this;\n\n return new Promise(function sandbox$$assignEvaluatorPromise(resolve, reject) {\n const message = {\n request: 'assign',\n job: ceci.job,\n sandboxConfig: dcpConfig.worker.sandbox,\n };\n\n /* note - onFailListener used for removal. This is necessary due to a bug in ee.once. /wg Feb 2022 */\n \n const onSuccess = (event) => {\n // eslint-disable-next-line no-use-before-define\n ceci.ee.removeListener('reject', onFailListener);\n ceci.emit('assigned', event.jobAddress);\n debug('Job assigned to evaluator');\n resolve();\n };\n\n const onFail = (error) => {\n // eslint-disable-next-line no-use-before-define\n ceci.ee.removeListener('assigned', onSuccessListener);\n reject(error);\n };\n\n const onSuccessListener = ceci.ee.once('assigned', onSuccess);\n const onFailListener = ceci.ee.once('reject', onFail);\n ceci.evaluatorHandle.postMessage(message);\n });\n }\n\n /**\n * Evaluates a string inside the sandbox.\n *\n * @param {string} code - the code to evaluate in the sandbox\n * @param {string} filename - the name of the 'file' to help with debugging,\n * no longer working though?\n * @returns {Promise} - resolves with eval result on success, rejects\n * otherwise\n */\n eval(code, filename) {\n var ceci = this;\n \n return new Promise(function sandbox$$eval$Promise(resolve, reject) {\n let msgId = nanoid();\n let msg = {\n request: 'eval',\n data: code,\n filename,\n msgId, \n }\n\n const eventId = EVAL_RESULT_PREFIX + msgId;\n\n let onSuccess = (event) => {\n ceci.ee.removeListener('reject', onFailListener)\n resolve(event)\n }\n\n let onFail = (event) => {\n ceci.ee.removeListener(eventId, onSuccessListener)\n reject(event)\n }\n\n let onSuccessListener = ceci.ee.once(eventId, onSuccess);\n let onFailListener = ceci.ee.once('reject', onFail)\n\n ceci.evaluatorHandle.postMessage(msg)\n })\n }\n\n /**\n * Resets the state of the bootstrap, without resetting the sandbox function if assigned.\n * Mostly used to reset the progress status before reusing a sandbox on another slice.\n * Must be called after @start.\n *\n * @returns {Promise} - resolves with result on success, rejects otherwise\n */\n resetSandboxState () {\n var ceci = this;\n\n return new Promise(function sandbox$resetSandboxStatePromise(resolve, reject) {\n let successCb, failTimeout;\n let msg = {\n request: 'resetState',\n };\n\n successCb = ceci.ee.once('resetStateDone', function sandbox$resetSandboxState$success () {\n if (failTimeout === false)\n return; /* already rejected */\n clearTimeout(failTimeout);\n failTimeout = false;\n resolve();\n });\n\n failTimeout = setTimeout(function sandbox$resetSandboxState$fail() {\n if (failTimeout === false)\n return; /* already resolved */\n \n ceci.ee.removeListener('resetStateDone', successCb);\n ceci.terminate(false);\n failTimeout = false;\n\n reject(new Error('resetState never received resetStateDone event from sandbox'));\n }, 3000 * timeDilation); /* XXXwg need tuneable */\n\n assert(ceci.evaluatorHandle); // It is possible that ceci.terminate nulls out evaluatorHandle before getting here.\n ceci.evaluatorHandle.postMessage(msg);\n });\n }\n\n /**\n * Clear all timers that are set inside the sandbox (evaluator) environment.\n *\n * @returns {Promise} - resolves with result on success, rejects otherwise\n */\n clearSandboxTimers() {\n var ceci = this;\n \n return new Promise(function sandbox$clearSandboxTimersPromise(resolve, reject) {\n let successCb, failTimeout;\n let msg = {\n request: 'clearTimers',\n };\n\n successCb = ceci.ee.once('clearTimersDone', function sandbox$clearSandboxTimers$success() {\n if (failTimeout === false)\n return; /* already rejected */\n clearTimeout(failTimeout);\n failTimeout = false;\n resolve();\n });\n\n failTimeout = setTimeout(function sanbox$clearSandboxTimers$fail() {\n if (failTimeout === false)\n return; /* already resolved */\n \n ceci.ee.removeListener('clearTimersDone', successCb);\n ceci.terminate(false);\n failTimeout = false;\n \n reject(new Error('clearTimers never received clearTimersDone event from sandbox'));\n }, 3000 * timeDilation); /* XXXwg need tuneable */\n\n if (ceci.evaluatorHandle) // Sometimes ceci.terminate nulls out evaluatorHandle before getting here.\n ceci.evaluatorHandle.postMessage(msg);\n });\n }\n\n /**\n * Sends a post message to describe its capabilities.\n *\n * Side effect: Sets the capabilities property of the current sandbox.\n *\n * @returns {Promise} Resolves with the sandbox's capabilities. Rejects with\n * an error saying a response was not received.\n * @memberof Sandbox\n */\n describe() {\n debug('Beginning to describe evaluator');\n var ceci = this;\n \n return new Promise(function sandbox$describePromise(resolve, reject) {\n if (ceci.evaluatorHandle === null) {\n return reject(new Error('Evaluator has not been initialized.'));\n }\n\n /**\n * Opted to create a flag for the describe response being received so that\n * we don't have to *hoist* the timeout's id to clear it in the response\n * handler.\n */\n let didReceiveDescribeResponse = false;\n const describeResponseHandler = ceci.ee.once('describe', (data) => {\n didReceiveDescribeResponse = true;\n const { capabilities } = data;\n if (typeof capabilities === 'undefined') {\n reject(\n new Error('Did not receive capabilities from describe response.'),\n );\n }\n ceci.capabilities = capabilities;\n\n // Currently only used in tests. May use the event in the future.\n ceci.emit('described', capabilities);\n debug('Evaluator has been described');\n resolve(capabilities);\n });\n const describeResponseFailedHandler = () => {\n if (!didReceiveDescribeResponse) {\n ceci.ee.removeListener('describe', describeResponseHandler);\n ceci.terminate(false);\n reject(\n new Error(\n 'Describe message timed-out. No describe response was received from the describe command.',\n ),\n );\n }\n };\n\n const message = {\n request: 'describe',\n };\n\n // Arbitrarily set the waiting time.\n setTimeout(describeResponseFailedHandler, 6000 * timeDilation); /* XXXwg need tuneable */\n assert(ceci.evaluatorHandle); // It is possible that ceci.terminate nulls out evaluatorHandle before getting here.\n ceci.evaluatorHandle.postMessage(message);\n });\n }\n\n /**\n * Passes the job's requirements object into the sandbox so that the global\n * access lists can be updated accordingly.\n *\n * e.g. disallow access to OffscreenCanvas without\n * environment.offscreenCanvas=true present.\n *\n * Must be called after @start.\n *\n * @returns {Promise} - resolves with result on success, rejects otherwise\n */\n applySandboxRequirements(requirements) {\n var ceci = this;\n \n return new Promise(function sandbox$applySandboxRequirementsPromise(resolve, reject) {\n const message = {\n requirements,\n request: 'applyRequirements',\n };\n let wereRequirementsApplied = false;\n\n const successCb = ceci.ee.once(\n 'applyRequirementsDone',\n function sandbox$applyRequirements$success() {\n wereRequirementsApplied = true;\n resolve();\n },\n );\n\n assert(typeof message.requirements === 'object');\n ceci.evaluatorHandle.postMessage(message);\n\n setTimeout(function sandbox$finishApplySandboxRequirements() {\n if (!wereRequirementsApplied) {\n ceci.ee.removeListener('applyRequirementsDone', successCb);\n ceci.terminate(false);\n reject(\n new Error(\n 'applyRequirements never received applyRequirementsDone response from sandbox',\n ),\n );\n }\n }, 3000 * timeDilation); /* XXXwg needs tunable */\n });\n }\n\n /**\n * Executes a slice received from the supervisor.\n * Must be called after @start.\n *\n * @param {Slice} slice - bare minimum data required for the job/job code to be executed on\n * @param {number} [delay = 0] the delay that this method should wait before beginning work, used to avoid starting all sandboxes at once\n *\n * @returns {Promise} - resolves with result on success, rejects otherwise\n */\n\n async work (slice, delay = 0) {\n var ceci = this;\n\n if (!ceci.isAssigned) {\n throw new Error(\"Sandbox.run: Sandbox is not ready to work, state=\" + ceci.state);\n }\n\n ceci.state = WORKING;\n ceci.slice = slice;\n assert(slice);\n\n // cf. DCP-1720\n this.resetSliceTimeReport();\n \n // Now wait for the delay if provided, prevents many sandboxes starting at once from crashing the supervisor\n if (delay > 0) await new Promise(resolve => setTimeout(resolve, (delay + 1) * timeDilation));\n if (!ceci.isWorking) return; // sandbox.terminate could have been called during the delay timeout\n\n // Prepare the sandbox to begin work\n // will be replaced by `assign` message that should be called before emitting a `work` message\n if (ceci.jobAddress !== slice.jobAddress) {\n throw new Error(`Sandbox.run: Sandbox is already assigned and jobAddress doesn't match previous (${ceci.jobAddress} !== ${slice.jobAddress})`);\n }\n\n let sliceHnd = { job: ceci.public, sandbox: ceci };\n await ceci.resetSandboxState();\n if (!ceci.slice) {\n console.error(`Slice for job ${ceci.jobAddress} vanished during work initialization - aborting`);\n return;\n }\n\n let inputDatum;\n let dataError = false;\n try {\n if (ceci.slice.datumUri)\n inputDatum = await fetchURI(ceci.slice.datumUri, this.supervisor.makeSafeOriginList('fetchData'));\n else {\n let { mro } = await ceci.supervisorCache.fetchJob(\n ceci.jobAddress,\n this.supervisor.makeSafeOriginList('fetchData')\n );\n const ro = rehydrateRange(mro);\n // -1 to prevent an OBOE since slice numbers start at 1.\n inputDatum = ro[ceci.slice.sliceNumber - 1];\n }\n } catch (err) {\n dataError = err;\n if(err.code === 'EFETCH_BAD_ORIGIN')\n dataError.errorCode = 'EFETCH_BAD_ORIGIN'\n else\n dataError.errorCode = 'EUNCAUGHTERROR'\n ceci.emit('workEmit', {\n eventName: 'error',\n payload: {\n message: dataError.message,\n stack:dataError.stack,\n name: ceci.public.name\n }\n });\n }\n\n debugging('sandbox') && debug(`Fetched datum: ${inputDatum}`);\n\n if (!ceci.slice) {\n console.error(`Slice for job ${ceci.jobAddress} vanished after data fetch - aborting`);\n return;\n }\n\n ceci.resetProgressTimeout();\n ceci.resetSliceTimeout();\n\n return new Promise(function sandbox$$workPromise(resolve, reject) {\n let onSuccess, onFail\n\n onSuccess = ceci.ee.once('resolve', function sandbox$$work$success (event) {\n ceci.ee.removeListener('reject', onFail)\n resolve(event)\n }.bind(ceci))\n\n onFail = ceci.ee.once('reject', function sandbox$$work$fail (err) {\n ceci.ee.removeListener('resolve', onSuccess)\n reject(err)\n }.bind(ceci))\n\n ceci.sliceStartTime = Date.now();\n ceci.progress = null;\n ceci.progressReports = {\n last: undefined,\n lastDeterministic: undefined,\n };\n\n ceci.resetProgressTimeout();\n ceci.resetSliceTimeout();\n ceci.emit('start', sliceHnd);\n \n if(dataError){\n ceci.ee.removeListener('resolve', onSuccess);\n ceci.ee.removeListener('reject', onFail);\n setTimeout(() => reject(dataError), 0)\n\n } else {\n ceci.evaluatorHandle.postMessage({\n request: 'main',\n data: inputDatum,\n })\n }\n })\n .then(async function sandbox$$work$then(event) {\n // Ceci is the complete callback when the slice completes\n // prevent any hanging timers from being fired\n await ceci.clearSandboxTimers();\n\n // TODO: Should sliceHnd just be replaced with ceci.public?\n ceci.emit('slice', sliceHnd); /** @todo: decide which event is right */\n ceci.emit('sliceFinish', event);\n ceci.emit('complete', event);\n\n ceci.changeWorkingToAssigned();\n ceci.slice = false;\n return event;\n })\n .catch((err) => { \n if (err.name === 'EWORKREJECT') {\n this.rejectionData = err;\n ceci.evaluatorHandle.postMessage({ request: 'resetAndGetCPUTime' })\n } else { // sandbox termination for rejected work happens in Supervisor.handleRejectedWork\n // Ceci is the reject callback for when the slice throws an error\n ceci.terminate(false);\n }\n\n ceci.emit('error', err, 'slice');\n\n if (err instanceof NoProgressError) {\n ceci.emit('workEmit', {\n eventName: 'noProgress',\n payload: {\n timestamp: Date.now() - ceci.sliceStartTime,\n data: ceci.slice.datumUri,\n progressReports: ceci.progressReports,\n }\n });\n }\n throw err;\n })\n .finally(function sandbox$$work$finally() {\n ceci.emit('end', sliceHnd);\n });\n }\n\n resetProgressTimeout() {\n if (this.progressTimeout) {\n clearTimeout(this.progressTimeout);\n this.progressTimeout = null;\n }\n\n this.progressTimeout = setTimeout(() => {\n if (this.options.ignoreNoProgress) {\n return console.warn(\"ENOPROGRESS silenced by localExec: In a remote worker, this slice would be stopped for not calling progress frequently enough.\");\n }\n\n this.ee.emit('reject', new NoProgressError(`No progress event was received in the last ${dcpConfig.worker.sandbox.progressTimeout / 1000} seconds.`));\n }, +dcpConfig.worker.sandbox.progressTimeout * timeDilation);\n }\n\n resetSliceTimeout() {\n if (this.sliceTimeout) clearTimeout(this.sliceTimeout);\n\n this.sliceTimeout = setTimeout(() => {\n if (Sandbox.debugWork) return console.warn(\"Sandbox.debugWork: Ignoring slice timeout\");\n\n this.ee.emit('reject', new SliceTooSlowError(`Slice took longer than ${dcpConfig.worker.sandbox.sliceTimeout / 1000} seconds.`));\n }, +dcpConfig.worker.sandbox.sliceTimeout * timeDilation);\n }\n \n async handleRing0Message(data) {\n debugging('event:ring-0') && debug('event:ring-0', data);\n //handling a true ring 0 message\n switch (data.request) {\n case 'sandboxLoaded':\n // emit externally\n this.emit('sandboxLoaded', this)\n break;\n\n case 'scriptLoaded':\n // emit externally\n this.emit('scriptLoaded', data);\n \n if(data.result !== \"success\") {\n this.onerror(data);\n }\n break;\n \n case 'clearTimersDone':\n this.ee.emit(data.request, data);\n break;\n case 'totalCPUTime':\n this.updateTime(data);\n if (this.ee.listenerCount('resolve') > 0) {\n this.completeData.timeReport = this.sliceTimeReport;\n this.ee.emit('resolve', this.completeData);\n delete this.completeData;\n } else {\n this.rejectionData.timeReport = this.sliceTimeReport\n this.emit('rejectedWorkMetrics', this.rejectionData) // If there is no internal listener for 'resolve', the slice was rejected and\n delete this.rejectionData; // we need to send the rejected metrics to the supervisor\n } \n break;\n case 'error':\n // Warning: rejecting here with just event.data.error causes issues\n // where the reject handlers modify the object so it interferes with the\n // workEmit event payload, wrapping in an Error instance copies the values\n let e = new Error(\n data.error.message,\n data.error.fileName,\n data.error.lineNumber);\n e.stack = data.error.stack;\n e.name = data.error.name;\n \n if (this.ee.listenerCount('reject') > 0) {\n this.ee.emit('reject', e);\n } else {\n // This will happen if the error is thrown during initialization\n throw e;\n }\n\n break;\n default:\n let error = new Error('Received unhandled request from sandbox: ' + data.request + '\\n\\t' + JSON.stringify(data));\n console.error(error);\n break; \n }\n }\n\n async handleRing1Message(data) {\n switch (data.request) {\n case 'applyRequirementsDone':\n // emit internally\n this.ee.emit(data.request, data)\n break;\n default:\n let error = new Error('Received unhandled request from sandbox ring 1: ' + data.request + '\\n\\t' + JSON.stringify(data));\n console.error(error)\n break; \n }\n }\n\n async handleRing2Message(data) {\n debugging('event:ring-2') && debug('event:ring-2', data);\n switch (data.request) {\n case 'dependency': {\n let moduleData;\n try {\n moduleData = await this.supervisorCache.fetchModule(data.data);\n } catch (error) {\n /*\n * In the event of an error here, we want to let the client know there was a problem in\n * loading their module. However, there hasn't yet been an actual slice assigned to the sandbox.\n * Therefore, we assign 'slice 0' to the sandbox, a slice that will never exist, and is used\n * purely for this purpose. \n */\n this.slice = {\n jobAddress: this.jobAddress,\n sliceNumber: 0,\n };\n\n const payload = {\n name: error.name,\n timestamp: error.timestamp,\n message: error.message,\n };\n\n this.emit('workEmit', {\n eventName: 'error',\n payload,\n });\n this.ee.emit('reject', error);\n break;\n }\n this.evaluatorHandle.postMessage({\n request: 'moduleGroup',\n data: moduleData,\n id: data.id,\n });\n break;\n }\n case 'error':\n /*\n * Ring 2 error messages will only fire for problems inside of the worker that are separate from\n * the work function. In most cases there are other handlers for situations where 'error' may be emitted\n * such as timeouts if the expected message isn't recieved. Thus, we will output the error, but nothing else.\n */\n console.error(data.error);\n break;\n case 'describe':\n case 'evalResult':\n case 'resetStateDone':\n case 'assigned':\n // emit internally\n this.ee.emit(data.request, data);\n break;\n case 'reject':\n // emit internally\n this.ee.emit(data.request, data.error);\n break;\n default: {\n const error = new Error(\n `Received unhandled request from sandbox ring 2. Data: ${JSON.stringify(\n data,\n null,\n 2,\n )}`,\n );\n console.error(error);\n break;\n }\n }\n }\n\n async handleRing3Message(data) {\n switch (data.request) {\n case 'complete':\n clearTimeout(this.progressTimeout);\n clearTimeout(this.sliceTimeout);\n this.progressTimeout = this.sliceTimeout = null;\n\n if (this.progress === null) {\n if (this.options.ignoreNoProgress) {\n console.warn(\"ENOPROGRESS silenced by localExec: Progress was not called during this slice's execution, in a remote sandbox this would cause the slice to be failed\");\n } else {\n // If a progress update was never received (progress === null) then reject\n this.ee.emit('reject', new NoProgressError('Sandbox never emitted a progress event.'));\n break;\n }\n }\n this.evaluatorHandle.postMessage({ request: 'resetAndGetCPUTime' })\n this.progress = 100;\n this.completeData = data;\n // The timing report and resolve will be emitted when the CPU time is received. \n break;\n case 'progress':\n let { progress, indeterminate, throttledReports, value } = data;\n this.progress = progress;\n const progressReport = {\n timestamp: Date.now() - this.sliceStartTime,\n progress,\n value,\n throttledReports,\n }\n this.progressReports.last = progressReport;\n if (!indeterminate) {\n this.progressReports.lastDeterministic = progressReport;\n }\n\n this.resetProgressTimeout();\n\n this.emit('sliceProgress', data);\n break;\n\n case 'noProgress':\n let { message } = data;\n\n this.ee.emit('reject', new NoProgressError(message));\n break;\n case 'console':\n data.payload.message = scopedKvin.marshal(data.payload.message); \n this.emit('workEmit', {\n eventName: 'console',\n payload: data.payload, \n });\n break;\n\n case 'emitEvent':/* ad-hoc event from the sandbox (work.emit) */\n this.emit('workEmit', {\n eventName: 'custom',\n payload: data.payload\n })\n break;\n case 'measurement':\n this.updateTime(data);\n break;\n case 'sandboxError': /* the sandbox itself has an error condition */\n {\n this.emit('sandboxError', data.error);\n break;\n }\n case 'workError': { /* the work function threw/rejected */\n this.emit('workEmit', {\n eventName: 'error',\n payload: data.error,\n });\n\n // Warning: rejecting here with just .data.error causes issues\n // where the reject handlers modify the object so it interferes with the\n // workEmit payload, wrapping in an Error instance copies the values\n const wrappedError = new UncaughtExceptionError(\n data.error.message,\n data.error.fileName,\n data.error.lineNumber,\n );\n wrappedError.stack = data.error.stack;\n wrappedError.name = data.error.name;\n\n if (this.ee.listenerCount('reject') > 0) {\n this.ee.emit('reject', wrappedError);\n } else {\n // This will happen if the error is thrown during initialization\n throw wrappedError;\n }\n break;\n }\n default:\n let error = new Error('Received unhandled request from sandbox ring 3: ' + data.request + '\\n\\t' + JSON.stringify(data));\n console.error(error)\n break; \n }\n }\n\n /**\n * Handles progress and completion events from sandbox.\n * Unless explicitly returned out of this function will re-emit the event\n * on @this.ee where the name of the event is event.data.request.\n *\n * @param {object} event - event received from the sandbox\n */\n async onmessage(event) {\n debugging('event') && debug('event', event);\n if (Sandbox.debugEvents) {\n console.debug('sandbox - eventDebug:', {\n id: this.id,\n state: this.state,\n event: JSON.stringify(event)\n })\n }\n\n const { data } = event;\n const ringLevel = data.ringSource\n\n // Give the data to a handler depending on ring level\n if (ringLevel === -1) {\n console.error('Message sent directly from raw postMessage. Terminating worker...');\n console.debug(event);\n return this.terminate(true);\n } else {\n const handler = this.ringMessageHandlers[ringLevel];\n if (handler) {\n handler.call(this, data.value);\n } else {\n console.warn(`No handler defined for message from ring ${ringLevel}`);\n console.debug(event);\n }\n }\n }\n\n /**\n * Error handler for the internal sandbox.\n * Currently just logs the errors that the sandbox spits out.\n */\n onerror(event) {\n console.error('Sandbox emitted an error:', event);\n this.terminate(true, true);\n }\n\n /**\n * Clears the timeout and terminates the sandbox and sometimes emits a reject event.\n *\n * @param {boolean} [reject = true] - if true emit reject event\n * @param {boolean} [immediate = false] - passed to terminate, used by standaloneWorker to immediately close the connection\n */\n terminate (reject = true, immediate = false) {\n const oldState = this.state;\n this.state = TERMINATED;\n \n clearTimeout(this.progressTimeout);\n clearTimeout(this.sliceTimeout);\n this.progressTimeout = this.sliceTimeout = null;\n \n if (this.evaluatorHandle && typeof this.evaluatorHandle.terminate === 'function') {\n try {\n this.evaluatorHandle.terminate(immediate);\n this.evaluatorHandle = null;\n } catch (e) {\n console.error(`Error terminating sandbox ${this.id} (${oldState}):`, e);\n } finally {\n this.emit('terminate', this);\n }\n }\n\n if (reject) {\n this.ee.emit('reject', new Error(`Sandbox ${this.id} (${oldState}) was terminated.`));\n }\n\n this.emit('terminated');\n }\n\n /**\n * Attempts to stop the sandbox from doing completing its current\n * set of work without terminating the working.\n */\n stop () {\n throw new Error('Sandbox.stop is not yet implemented.')\n }\n\n /**\n * ringNPostMessage can send a `measurement` request and update these\n * totals.\n */\n updateTime (measurementEvent) {\n ['total', 'CPU', 'webGL'].forEach((key) => {\n if (measurementEvent[key]) this.sliceTimeReport[key] += measurementEvent[key];\n })\n }\n\n resetSliceTimeReport () {\n this.sliceTimeReport = {\n total: 0,\n CPU: 0,\n webGL: 0,\n }\n }\n}\n\nSandbox.idCounter = 1;\nSandbox.debugWork = false;\nSandbox.debugState = false;\nSandbox.debugEvents = false;\n\nexports.Sandbox = Sandbox;\nexports.SandboxError = SandboxError;\nexports.NoProgressError = NoProgressError;\nexports.SliceTooSlowError = SliceTooSlowError;\nexports.UncaughtExceptionError = UncaughtExceptionError;\nexports.RemoteFetchError = RemoteFetchError;\n\n\n//# sourceURL=webpack://dcp/./src/dcp-client/worker/sandbox.js?");
4428
4484
 
4429
4485
  /***/ }),
4430
4486
 
@@ -4435,7 +4491,7 @@ eval("// NOTE - need timeout/postmessage function\n/**\n * @file dcp-client/work
4435
4491
  /***/ ((__unused_webpack_module, exports) => {
4436
4492
 
4437
4493
  "use strict";
4438
- eval("/**\n * @file worker/slice.js\n *\n * A wrapper for the slice object returned from the scheduler.\n * It has useful helper functions such as @getNextSlice and @getResultsMessage\n *\n *\n * @author Matthew Palma, mpalma@kingsds.network\n * Ryan Rossiter, ryan@kingsds.network\n * @date May 2019\n */\n\n/* global dcpConfig, protocol */\n\n\nconst SLICE_STATUS_UNASSIGNED = 'UNASSIGNED';\nconst SLICE_STATUS_WORKING = 'WORKING';\nconst SLICE_STATUS_COMPLETE = 'COMPLETE';\nconst SLICE_STATUS_FAILED = 'FAILED';\n\n/** \n * Object use to represent a given slice inside the Supervisor. This object's shape \n * current inherits heavily from the message payload originating from the scheduler,\n * but should not /wg dec 2020.\n *\n * Caveat lector: this documentation was created without a 100% understanding of the\n * code. Please improve as possible.\n *\n * The properties of this object are as follows:\n * - sliceNumber the number of the slice within this job\n * - jobAddress the address of the job that this slice belongs to\n * - datumUri the URI describing the data (input set element) for \n * this slice of the job; could be a data: URI or it\n * could be a URL we need to fetch; note that fetches \n * are limited to worker's allow list\n * - resultStorageType 'values' => we are storing individual values,\n * which could be data: URIs or URLs\n * at the scheduler\n * 'pattern' => user-specified pattern for result URLs.\n * Data will be uploaded via POST to the\n * URL matching the pattern, provided the\n * worker is allowed to access it.\n * - resultStorageParams user-supplied POST parameters sent to the result \n * storage server when using resultStorageType = pattern.\n * - resultStorageDetails the pattern when using resultStorageType = pattern.\n * - result\n * - result.request 'complete' | ???\n * - result.result return value from work function\n * - result.timeReport { total, idle } ms \n *\n * - status COMPLETE | FAILED | WORKING | UNASSIGNED\n */\nclass Slice {\n constructor (taskElement, authorizationMessage) {\n /** @deprecated - UNUSED */\n this.index = taskElement.sliceNumber; /* deprecated - do not use */\n /** @type {string} */\n this.status = SLICE_STATUS_UNASSIGNED;\n /** @type {boolean} */\n this.allocated = false;\n /** @type {boolean} */\n this.isEstimationSlice = taskElement.isEstimationSlice;\n /** @type {boolean} */\n this.isLongSlice = taskElement.isLongSlice;\n /** @type {number} */\n this.sliceNumber = taskElement.sliceNumber;\n this.jobAddress = taskElement.jobAddress;\n this.datumUri = taskElement.datumUri;\n this.resultStorageType = taskElement.resultStorageType;\n this.resultStorageParams = taskElement.resultStorageParams;\n this.resultStorageDetails = taskElement.resultStorageDetails;\n this.authorizationMessage = authorizationMessage;\n this.result = null;\n this.error = undefined;\n this.rejected = null;\n this.rejectedTimeReport = null;\n }\n\n get identifier() {\n if (this.allocated)\n return `${this.sliceNumber}.${this.jobAddress}.${this.status}.${this.allocated}`;\n return `${this.sliceNumber}.${this.jobAddress}.${this.status}`;\n }\n\n /**\n * @returns {boolean} - true if the slice is ready to be submitted \n */\n get isFinished() {\n return this.status === SLICE_STATUS_COMPLETE || this.status === SLICE_STATUS_FAILED;\n }\n\n get failed () {\n return this.status === SLICE_STATUS_FAILED;\n }\n\n get completed () {\n return this.status === SLICE_STATUS_COMPLETE;\n }\n\n get isUnassigned () {\n return this.status === SLICE_STATUS_UNASSIGNED\n }\n\n /**\n * @returns {boolean} - true if the slice has been handed out, does not imply slice is complete\n */\n get isWorking () {\n return this.status === SLICE_STATUS_WORKING;\n }\n\n get timeReport () {\n const timeReport = this.result.timeReport;\n if (timeReport) {\n const rejectedTimeReport = this.rejectedTimeReport; // Data collected from sandboxes that rejected this slice.\n if (rejectedTimeReport && rejectedTimeReport.total > 0) {\n ['total', 'CPU', 'webGL'].forEach((key) => {\n if (rejectedTimeReport[key]) timeReport[key] += rejectedTimeReport[key];\n });\n }\n }\n return timeReport;\n }\n\n // Sets the slice status to working, called when the slice is handed to a worker\n markAsWorking() {\n if (!this.isUnassigned) throw new Error(`Slice ${this.identifier} is already assigned, can't mark as working.`);\n this.status = SLICE_STATUS_WORKING;\n }\n\n /**\n * Receives a result from the scheduler.\n * It will then put the result in the appropriate place.\n * It could also eventually determine if the slice should be\n * retried before determining that it has failed.\n *\n * @param {object|Error} result - the result that came back from the worker\n * @param {boolean} [success = true] - true is result is considered successful, false if something went wrong computing\n */\n collectResult (result, success = true) {\n if (this.result)\n throw new Error(`Slice ${this.identifier} received more than one result.`);\n \n if (success) {\n this.status = SLICE_STATUS_COMPLETE;\n this.result = result;\n } else {\n this.status = SLICE_STATUS_FAILED;\n this.error = result;\n }\n }\n\n /**\n * (Currently unused.)\n * Create and return the payload object to send back to the result submitter\n */\n getResultMessagePayload () {\n if (!this.hasOwnProperty('result'))\n throw new Error(`Cannot create result message payload for slice ${this.identifier}.`);\n\n return {\n job: this.jobAddress,\n slice: this.sliceNumber,\n result: this.result,\n authorizationMessage: this.getAuthorizationMessage(),\n }; \n }\n\n /**\n * Create and return a slice-return payload to send to the\n * result-submitter's status operation\n *\n * @param {address} worker The current worker's opaque\n * @param {string} [reason] Optional reason for the return: 'ENOPROGRESS', 'EUNCAUGHT', 'ESLICETOOSLOW', 'unknown'.\n * @return {object} Message payload; still requires worker address\n */\n getReturnMessagePayload (worker, reason) {\n delete this.result;\n\n if (!reason)\n reason = this.error ? 'EUNCAUGHT' : 'unknown';\n\n return {\n worker,\n slices: [{\n job: this.jobAddress,\n sliceNumber: this.sliceNumber,\n status: 'return',\n isEstimationSlice: this.isEstimationSlice,\n authorizationMessage: this.getAuthorizationMessage(),\n reason,\n error: this.error\n }],\n };\n }\n\n /**\n * This function helps enforce the equivalence:\n * !this.authorizationMessage <==> sliceNumber === 0 .\n * @returns {object} this.authorizationMessage\n */\n getAuthorizationMessage () {\n if (!this.authorizationMessage && this.sliceNumber > 0)\n throw new Error(`Undefined authorization for slice ${this.identifier}.`);\n return this.authorizationMessage;\n }\n}\n\nexports.Slice = Slice;\nexports.SLICE_STATUS_UNASSIGNED = SLICE_STATUS_UNASSIGNED;\nexports.SLICE_STATUS_WORKING = SLICE_STATUS_WORKING;\nexports.SLICE_STATUS_COMPLETE = SLICE_STATUS_COMPLETE;\nexports.SLICE_STATUS_FAILED = SLICE_STATUS_FAILED;\n\n\n//# sourceURL=webpack://dcp/./src/dcp-client/worker/slice.js?");
4494
+ eval("/**\n * @file worker/slice.js\n *\n * A wrapper for the slice object returned from the scheduler.\n * It has useful helper functions such as @getNextSlice and @getResultsMessage\n *\n *\n * @author Matthew Palma, mpalma@kingsds.network\n * Ryan Rossiter, ryan@kingsds.network\n * @date May 2019\n */\n\n/* global dcpConfig, protocol */\n\n\nconst SLICE_STATUS_UNASSIGNED = 'UNASSIGNED';\nconst SLICE_STATUS_WORKING = 'WORKING';\nconst SLICE_STATUS_COMPLETE = 'COMPLETE';\nconst SLICE_STATUS_FAILED = 'FAILED';\n\n/** \n * Object use to represent a given slice inside the Supervisor. This object's shape \n * current inherits heavily from the message payload originating from the scheduler,\n * but should not /wg dec 2020.\n *\n * Caveat lector: this documentation was created without a 100% understanding of the\n * code. Please improve as possible.\n *\n * The properties of this object are as follows:\n * - sliceNumber the number of the slice within this job\n * - jobAddress the address of the job that this slice belongs to\n * - datumUri the URI describing the data (input set element) for \n * this slice of the job; could be a data: URI or it\n * could be a URL we need to fetch; note that fetches \n * are limited to worker's allow list\n * - resultStorageType 'values' => we are storing individual values,\n * which could be data: URIs or URLs\n * at the scheduler\n * 'pattern' => user-specified pattern for result URLs.\n * Data will be uploaded via POST to the\n * URL matching the pattern, provided the\n * worker is allowed to access it.\n * - resultStorageParams user-supplied POST parameters sent to the result \n * storage server when using resultStorageType = pattern.\n * - resultStorageDetails the pattern when using resultStorageType = pattern.\n * - result\n * - result.request 'complete' | ???\n * - result.result return value from work function\n * - result.timeReport { total, idle } ms \n *\n * - status COMPLETE | FAILED | WORKING | UNASSIGNED\n */\nclass Slice {\n constructor (taskElement, authorizationMessage) {\n /** @deprecated - UNUSED */\n this.index = taskElement.sliceNumber; /* deprecated - do not use */\n /** @type {string} */\n this.status = SLICE_STATUS_UNASSIGNED;\n /** @type {boolean} */\n this.allocated = false;\n /** @type {boolean} */\n this.isEstimationSlice = taskElement.isEstimationSlice;\n /** @type {boolean} */\n this.isLongSlice = taskElement.isLongSlice;\n /** @type {number} */\n this.sliceNumber = taskElement.sliceNumber;\n this.jobAddress = taskElement.jobAddress;\n this.datumUri = taskElement.datumUri;\n this.resultStorageType = taskElement.resultStorageType;\n this.resultStorageParams = taskElement.resultStorageParams;\n this.resultStorageDetails = taskElement.resultStorageDetails;\n this.authorizationMessage = authorizationMessage;\n this.result = null;\n this.error = undefined;\n this.rejected = null;\n this.rejectedTimeReport = null;\n }\n\n get identifier() {\n if (this.allocated)\n return `${this.sliceNumber}.${this.jobAddress}.${this.status}.${this.allocated}`;\n return `${this.sliceNumber}.${this.jobAddress}.${this.status}`;\n }\n\n /**\n * @returns {boolean} - true if the slice is ready to be submitted \n */\n get isFinished() {\n return this.status === SLICE_STATUS_COMPLETE || this.status === SLICE_STATUS_FAILED;\n }\n\n get failed () {\n return this.status === SLICE_STATUS_FAILED;\n }\n\n get completed () {\n return this.status === SLICE_STATUS_COMPLETE;\n }\n\n get isUnassigned () {\n return this.status === SLICE_STATUS_UNASSIGNED\n }\n\n /**\n * @returns {boolean} - true if the slice has been handed out, does not imply slice is complete\n */\n get isWorking () {\n return this.status === SLICE_STATUS_WORKING;\n }\n\n get timeReport () {\n const timeReport = this.result.timeReport;\n if (timeReport) {\n const rejectedTimeReport = this.rejectedTimeReport; // Data collected from sandboxes that rejected this slice.\n if (rejectedTimeReport && rejectedTimeReport.total > 0) {\n ['total', 'CPU', 'webGL'].forEach((key) => {\n if (rejectedTimeReport[key]) timeReport[key] += rejectedTimeReport[key];\n });\n }\n }\n return timeReport;\n }\n\n // Sets the slice status to working, called when the slice is handed to a worker\n markAsWorking() {\n if (!this.isUnassigned) throw new Error(`Slice ${this.identifier} is already assigned, can't mark as working.`);\n this.status = SLICE_STATUS_WORKING;\n }\n\n /**\n * Receives a result from the supervisor.\n * It will then put the result in the appropriate place.\n * It could also eventually determine if the slice should be\n * retried before determining that it has failed.\n *\n * @param {object|Error} result - the result that came back from the worker\n * @param {boolean} [success = true] - true is result is considered successful, false if something went wrong computing\n */\n collectResult (result, success = true) {\n if (this.result)\n throw new Error(`Slice ${this.identifier} received more than one result.`);\n \n if (success) {\n this.status = SLICE_STATUS_COMPLETE;\n this.result = result;\n } else {\n this.status = SLICE_STATUS_FAILED;\n this.error = result;\n }\n }\n\n /**\n * (Currently unused.)\n * Create and return the payload object to send back to the result submitter\n */\n getResultMessagePayload () {\n if (!this.hasOwnProperty('result'))\n throw new Error(`Cannot create result message payload for slice ${this.identifier}.`);\n\n return {\n job: this.jobAddress,\n slice: this.sliceNumber,\n result: this.result,\n authorizationMessage: this.getAuthorizationMessage(),\n }; \n }\n\n /**\n * Create and return a slice-return payload to send to the\n * result-submitter's status operation\n *\n * @param {address} worker The current worker's opaque\n * @param {string} [reason] Optional reason for the return: 'ENOPROGRESS', 'EUNCAUGHT', 'ESLICETOOSLOW', 'unknown'.\n * @return {object} Message payload; still requires worker address\n */\n getReturnMessagePayload (worker, reason) {\n delete this.result;\n\n if (!reason)\n reason = this.error ? 'EUNCAUGHT' : 'unknown';\n\n return {\n worker,\n slices: [{\n job: this.jobAddress,\n sliceNumber: this.sliceNumber,\n status: 'return',\n isEstimationSlice: this.isEstimationSlice,\n authorizationMessage: this.getAuthorizationMessage(),\n reason,\n error: this.error\n }],\n };\n }\n\n /**\n * This function helps enforce the equivalence:\n * !this.authorizationMessage <==> sliceNumber === 0 .\n * @returns {object} this.authorizationMessage\n */\n getAuthorizationMessage () {\n if (!this.authorizationMessage && this.sliceNumber > 0)\n throw new Error(`Undefined authorization for slice ${this.identifier}.`);\n return this.authorizationMessage;\n }\n}\n\nexports.Slice = Slice;\nexports.SLICE_STATUS_UNASSIGNED = SLICE_STATUS_UNASSIGNED;\nexports.SLICE_STATUS_WORKING = SLICE_STATUS_WORKING;\nexports.SLICE_STATUS_COMPLETE = SLICE_STATUS_COMPLETE;\nexports.SLICE_STATUS_FAILED = SLICE_STATUS_FAILED;\n\n\n//# sourceURL=webpack://dcp/./src/dcp-client/worker/slice.js?");
4439
4495
 
4440
4496
  /***/ }),
4441
4497
 
@@ -4445,7 +4501,7 @@ eval("/**\n * @file worker/slice.js\n *\n * A wrapper for the slice object retur
4445
4501
  \***************************************************/
4446
4502
  /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
4447
4503
 
4448
- eval("/**\n * @file worker/supervisor-cache.js\n *\n * A cache for the supervisor, anything the supervisor\n * may request that is cacheable (the same every time its requested)\n * can be cached in this class.\n *\n * Currently only jobs and modules are cached.\n *\n * @author Matthew Palma, mpalma@kingsds.network\n * @date May 2019\n */\n\n/* global dcpConfig */\n\nconst debugging = (__webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope)('worker');\nconst { justFetch, fetchURI, dumpObject } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\nconst { EventEmitter } = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\");\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\nconst DCP_ENV = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\n\nconst kvin = __webpack_require__(/*! kvin */ \"./node_modules/kvin/kvin.js\");\nconst scopedKvin = new kvin.KVIN({Object: ({}).constructor,\n Array: ([]).constructor, \n Function: (()=>{}).constructor});\n \nclass SupervisorCache extends EventEmitter {\n constructor (supervisor) {\n \n super('SupervisorCache')\n this.supervisor = supervisor;\n this.cache = {\n job: {},\n module: {},\n dependency: {},\n }\n \n this.promises = {\n job: {},\n module: {},\n dependency: {},\n }\n\n this.lastAccess = {}\n for (let key in this.cache) {\n this.lastAccess[key] = {}\n }\n }\n\n /**\n * Returns an object listing what jobs and modules are currently cached.\n * @returns {object} - in the form: { job: [0xgen1, 0xgen2,...], modules: [modGroup1, modGroup2,...] }\n */\n get cacheDescription () {\n let description = {}\n for (let key in this.cache) {\n description[key] = Object.keys(this.cache[key])\n }\n return description\n }\n\n /**\n * Returns an array of all jobId currently cached\n * @returns {Array} all the jobId's in the cache\n */\n get jobs () {\n return Object.keys(this.cache.job);\n }\n\n /**\n * Attempts to look up an item from the cache.\n * If item is found its last access time is updated.\n *\n * @param {string} key - the cache to look in (job or module)\n * @param {string} id - the items identifier (jobAddress or module group name)\n *\n * @returns {any} The value of the stored item or null if nothing is found\n */\n fetch(key, id) {\n if (typeof this.cache[key] === 'undefined') {\n throw new Error(`${key} does not relate to any cache.`);\n }\n if (this.cache[key][id]) {\n this.lastAccess[key][id] = Date.now();\n return this.cache[key][id];\n }\n return null;\n }\n\n /**\n * Stores a fetched value for one of the caches.\n *\n * @param {string} key - the cache to store the item in\n * @param {string} id - the items identifier (job Address or module group name)\n * @param {any} value - the item to store\n *\n * @returns {any} - @value passed in\n */\n store (key, id, value) {\n if (typeof this.cache[key] === 'undefined') {\n throw new Error(`${key} does not relate to any cache.`)\n }\n this.cache[key][id] = value\n this.lastAccess[key][id] = Date.now()\n return value\n }\n\n /**\n * Removes a job or module group from the cache.\n *\n * @param {string} key - the cache to remove the item from (jobs or module groups)\n * @param {string} id - the items identifier (job Address or module group name)\n *\n * @returns {string} - @id passed in\n */\n remove (key, id) {\n if (typeof this.cache[key] === 'undefined') {\n throw new Error(`${key} does not relate to any cache.`)\n }\n delete this.cache[key][id];\n delete this.lastAccess[key][id];\n return id;\n }\n\n /** \n * Fetch a job from the job cache. If the job has components which are \n * which need to be fetched over the network, they are fetched before the\n * returned promise is resolved.\n *\n * The job cache is initially populated during fetchTask.\n */\n async fetchJob(address, allowedOrigins) {\n let job = this.fetch('job', address);\n\n if (!job) {\n let e = new Error(`No job in supervisor cache with address ${address}`);\n e.code = 'ENOJOB';\n throw e;\n }\n\n // XXXpfr Excellent tracing.\n if (debugging('worker')) {\n dumpObject(job, 'SupervisorCache.fetchJob: job', 128);\n }\n\n if (!job.workFunction) {\n job.workFunction = await fetchURI(job.codeLocation, allowedOrigins, dcpConfig.worker.allowOrigins.fetchWorkFunctions);\n if(job.requirements.useStrict)\n job.useStrict = true;\n delete job.codeLocation;\n }\n if (!job.arguments) {\n let promises = [];\n let uris = job.argumentsLocation;\n if (uris)\n for (let i = 0; i < uris.length; i++)\n promises.push(fetchURI(uris[i].value, allowedOrigins, dcpConfig.worker.allowOrigins.fetchArguments));\n\n job.arguments = await Promise.all(promises);\n \n // node localExec jobs read arguments from a file, so need to ensure they are properly parsed after being read.\n if (this.supervisor.options.localExec && !DCP_ENV.isBrowserPlatform)\n job.arguments = scopedKvin.parse(job.arguments[0]);\n\n delete job.argumentsLocation;\n }\n // if job input data is range object, we send the range object URI to the worker\n if (!job.mro && job.MROLocation) {\n job.mro = await fetchURI(job.MROLocation, allowedOrigins, dcpConfig.worker.allowOrigins.fetchWorkFunctions);\n delete job.MROLocation;\n }\n \n return job;\n }\n\n /**\n * Attempts to fetch a module group from the cache and\n * if it's not found it attempts to fetch then store\n * the module group from the package manager.\n *\n * @param {array} modulesArray - the array of modules requested \n * - (when stringified it's the identifier of the module group)\n *\n * @returns {Promise<object>} - the module group\n * @throws when the module group can not be fetched\n */\n async fetchModule(modulesArray) {\n const cacheKey = JSON.stringify(modulesArray);\n let modules = this.fetch('module', cacheKey);\n if (modules !== null) {\n return modules;\n }\n\n if (this.promises.module[cacheKey]) {\n return this.promises.module[cacheKey];\n }\n\n const {\n success,\n payload: responsePayload,\n } = await this.supervisor.packageManagerConnection.send('fetchModule', {\n modules: modulesArray,\n });\n\n if (!success) {\n /**\n * Preserving the error message by not rewrapping it with DCPError incase\n * we want to let clients know which module couldn't be fetched.\n */\n throw responsePayload;\n }\n\n this.promises.module[cacheKey] = responsePayload;\n modules = await this.promises.module[cacheKey];\n delete this.promises.module[cacheKey];\n return this.store('module', cacheKey, modules);\n }\n\n /**\n * Attempts to fetch a dependency from the cache and\n * if it's not found it attempts to fetch then store\n * the dependency from the package manager.\n *\n * @param {string} dependencyUri - The URI of the dependency\n *\n * @returns {Promise<string>} file contents\n * @throws when the dependency can not be fetched\n */\n async fetchDependency(dependencyUri) {\n let dependency = this.fetch('dependency', dependencyUri);\n if (dependency !== null) {\n return dependency;\n }\n\n if (this.promises.dependency[dependencyUri]) {\n return this.promises.dependency[dependencyUri];\n }\n\n const url = dcpConfig.packageManager.location.resolve(dependencyUri);\n this.promises.dependency[dependencyUri] = justFetch(url, 'string', 'GET', true);\n\n dependency = await this.promises.dependency[dependencyUri];\n\n delete this.promises.dependency[dependencyUri];\n\n return this.store('dependency', dependencyUri, dependency);\n }\n}\n\nexports.SupervisorCache = SupervisorCache;\n\n\n//# sourceURL=webpack://dcp/./src/dcp-client/worker/supervisor-cache.js?");
4504
+ eval("/**\n * @file worker/supervisor-cache.js\n *\n * A cache for the supervisor, anything the supervisor\n * may request that is cacheable (the same every time its requested)\n * can be cached in this class.\n *\n * Currently only jobs and modules are cached.\n *\n * @author Matthew Palma, mpalma@kingsds.network\n * @date May 2019\n */\n\n/* global dcpConfig */\n\nconst debugging = (__webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope)('worker');\nconst { justFetch, fetchURI, dumpObject } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\nconst { EventEmitter } = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\");\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\nconst DCP_ENV = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\n\nconst kvin = __webpack_require__(/*! kvin */ \"./node_modules/kvin/kvin.js\");\nconst scopedKvin = new kvin.KVIN({Object: ({}).constructor,\n Array: ([]).constructor, \n Function: (()=>{}).constructor});\n \nclass SupervisorCache extends EventEmitter {\n constructor (supervisor) {\n \n super('SupervisorCache')\n this.supervisor = supervisor;\n this.cache = {\n job: {},\n module: {},\n dependency: {},\n }\n \n this.promises = {\n job: {},\n module: {},\n dependency: {},\n }\n\n this.lastAccess = {}\n for (let key in this.cache) {\n this.lastAccess[key] = {}\n }\n }\n\n /**\n * Returns an object listing what jobs and modules are currently cached.\n * @returns {object} - in the form: { job: [0xgen1, 0xgen2,...], modules: [modGroup1, modGroup2,...] }\n */\n get cacheDescription () {\n let description = {}\n for (let key in this.cache) {\n description[key] = Object.keys(this.cache[key])\n }\n return description\n }\n\n /**\n * Returns an array of all jobId currently cached\n * @returns {Array} all the jobId's in the cache\n */\n get jobs () {\n return Object.keys(this.cache.job);\n }\n\n /**\n * Attempts to look up an item from the cache.\n * If item is found its last access time is updated.\n *\n * @param {string} key - the cache to look in (job or module)\n * @param {string} id - the items identifier (jobAddress or module group name)\n *\n * @returns {any} The value of the stored item or null if nothing is found\n */\n fetch(key, id) {\n if (typeof this.cache[key] === 'undefined') {\n throw new Error(`${key} does not relate to any cache.`);\n }\n if (this.cache[key][id]) {\n this.lastAccess[key][id] = Date.now();\n return this.cache[key][id];\n }\n return null;\n }\n\n /**\n * Stores a fetched value for one of the caches.\n *\n * @param {string} key - the cache to store the item in\n * @param {string} id - the items identifier (job Address or module group name)\n * @param {any} value - the item to store\n *\n * @returns {any} - @value passed in\n */\n store (key, id, value) {\n if (typeof this.cache[key] === 'undefined') {\n throw new Error(`${key} does not relate to any cache.`)\n }\n this.cache[key][id] = value\n this.lastAccess[key][id] = Date.now()\n return value\n }\n\n /**\n * Removes a job or module group from the cache.\n *\n * @param {string} key - the cache to remove the item from (jobs or module groups)\n * @param {string} id - the items identifier (job Address or module group name)\n *\n * @returns {string} - @id passed in\n */\n remove (key, id) {\n if (typeof this.cache[key] === 'undefined') {\n throw new Error(`${key} does not relate to any cache.`)\n }\n delete this.cache[key][id];\n delete this.lastAccess[key][id];\n return id;\n }\n\n /** \n * Fetch a job from the job cache. If the job has components which are \n * which need to be fetched over the network, they are fetched before the\n * returned promise is resolved.\n *\n * The job cache is initially populated during fetchTask.\n */\n async fetchJob(address, safeOriginListFactory) {\n let job = this.fetch('job', address);\n\n if (!job) {\n let e = new Error(`No job in supervisor cache with address ${address}`);\n e.code = 'ENOJOB';\n throw e;\n }\n\n // XXXpfr Excellent tracing.\n if (debugging('worker')) {\n dumpObject(job, 'SupervisorCache.fetchJob: job', 128);\n }\n\n if (!job.workFunction) {\n job.workFunction = await fetchURI(job.codeLocation, safeOriginListFactory('fetchWorkFunctions'));\n if(job.requirements.useStrict)\n job.useStrict = true;\n delete job.codeLocation;\n }\n if (!job.arguments) {\n let promises = [];\n let uris = job.argumentsLocation;\n if (uris)\n for (let i = 0; i < uris.length; i++)\n promises.push(fetchURI(uris[i].value, safeOriginListFactory('fetchArguments')));\n\n job.arguments = await Promise.all(promises);\n\n delete job.argumentsLocation;\n }\n // if job input data is range object, we send the range object URI to the worker\n if (!job.mro && job.MROLocation) {\n job.mro = await fetchURI(job.MROLocation, safeOriginListFactory('fetchData'));\n delete job.MROLocation;\n }\n \n return job;\n }\n\n /**\n * Attempts to fetch a module group from the cache and\n * if it's not found it attempts to fetch then store\n * the module group from the package manager.\n *\n * @param {array} modulesArray - the array of modules requested \n * - (when stringified it's the identifier of the module group)\n *\n * @returns {Promise<object>} - the module group\n * @throws when the module group can not be fetched\n */\n async fetchModule(modulesArray) {\n const cacheKey = JSON.stringify(modulesArray);\n let modules = this.fetch('module', cacheKey);\n if (modules !== null) {\n return modules;\n }\n\n if (this.promises.module[cacheKey]) {\n return this.promises.module[cacheKey];\n }\n\n const {\n success,\n payload: responsePayload,\n } = await this.supervisor.packageManagerConnection.send('fetchModule', {\n modules: modulesArray,\n });\n\n if (!success) {\n /**\n * Preserving the error message by not rewrapping it with DCPError incase\n * we want to let clients know which module couldn't be fetched.\n */\n throw responsePayload;\n }\n\n this.promises.module[cacheKey] = responsePayload;\n modules = await this.promises.module[cacheKey];\n delete this.promises.module[cacheKey];\n return this.store('module', cacheKey, modules);\n }\n\n /**\n * Attempts to fetch a dependency from the cache and\n * if it's not found it attempts to fetch then store\n * the dependency from the package manager.\n *\n * @param {string} dependencyUri - The URI of the dependency\n *\n * @returns {Promise<string>} file contents\n * @throws when the dependency can not be fetched\n */\n async fetchDependency(dependencyUri) {\n let dependency = this.fetch('dependency', dependencyUri);\n if (dependency !== null) {\n return dependency;\n }\n\n if (this.promises.dependency[dependencyUri]) {\n return this.promises.dependency[dependencyUri];\n }\n\n const url = dcpConfig.packageManager.location.resolve(dependencyUri);\n this.promises.dependency[dependencyUri] = justFetch(url, 'string', 'GET', true);\n\n dependency = await this.promises.dependency[dependencyUri];\n\n delete this.promises.dependency[dependencyUri];\n\n return this.store('dependency', dependencyUri, dependency);\n }\n}\n\nexports.SupervisorCache = SupervisorCache;\n\n\n//# sourceURL=webpack://dcp/./src/dcp-client/worker/supervisor-cache.js?");
4449
4505
 
4450
4506
  /***/ }),
4451
4507
 
@@ -4456,7 +4512,81 @@ eval("/**\n * @file worker/supervisor-cache.js\n *\n * A cache for the superviso
4456
4512
  /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
4457
4513
 
4458
4514
  "use strict";
4459
- eval("/* provided dependency */ var process = __webpack_require__(/*! ./node_modules/process/browser.js */ \"./node_modules/process/browser.js\");\n/**\n * @file worker/supervisor.js\n *\n * The component that controls each of the sandboxes\n * and distributes work to them. Also communicates with the\n * scheduler to fetch said work.\n *\n * The supervisor readies sandboxes before/while fetching slices.\n * This means sometimes there are extra instantiated WebWorkers\n * that are idle (in this.readiedSandboxes). Readied sandboxes can\n * be used for any slice. After a readied sandbox is given a slice\n * it becomes assigned to slice's job and can only do work\n * for that job.\n *\n * After a sandbox completes its work, the sandbox becomes cached\n * and can be reused if another slice with a matching job is fetched.\n *\n * @author Matthew Palma, mpalma@kingsds.network\n * Ryan Rossiter, ryan@kingsds.network\n * @date May 2019\n */\n\n/* global dcpConfig */\n// @ts-check\n\n\nconst constants = __webpack_require__(/*! dcp/common/scheduler-constants */ \"./src/common/scheduler-constants.js\");\nconst hash = __webpack_require__(/*! dcp/common/hash */ \"./src/common/hash.js\");\nconst wallet = __webpack_require__(/*! dcp/dcp-client/wallet */ \"./src/dcp-client/wallet/index.js\");\nconst protocolV4 = __webpack_require__(/*! dcp/protocol-v4 */ \"./src/protocol-v4/index.js\");\nconst DCP_ENV = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\n\nconst debugging = (__webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope)('worker');\nconst { assert } = __webpack_require__(/*! dcp/common/dcp-assert */ \"./src/common/dcp-assert.js\");\nconst { EventEmitter } = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\");\nconst { Sandbox, SandboxError } = __webpack_require__(/*! ./sandbox */ \"./src/dcp-client/worker/sandbox.js\");\nconst { Slice, SLICE_STATUS_UNASSIGNED, SLICE_STATUS_FAILED } = __webpack_require__(/*! ./slice */ \"./src/dcp-client/worker/slice.js\");\nconst { SupervisorCache } = __webpack_require__(/*! ./supervisor-cache */ \"./src/dcp-client/worker/supervisor-cache.js\");\nconst { DcpURL } = __webpack_require__(/*! dcp/common/dcp-url */ \"./src/common/dcp-url.js\");\nconst { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\nconst { localStorage } = __webpack_require__(/*! dcp/common/dcp-localstorage */ \"./src/common/dcp-localstorage.js\");\nconst { booley, encodeDataURI, makeDataURI, leafMerge, a$sleepMs, justFetch, compressJobMap, toJobMap,\n compressSandboxes, compressSlices, truncateAddress, dumpSandboxesIfNotUnique, dumpSlicesIfNotUnique, generateOpaqueId } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\nconst { sliceStatus } = __webpack_require__(/*! dcp/common/scheduler-constants */ \"./src/common/scheduler-constants.js\");\nconst { calculateJoinHash } = __webpack_require__(/*! dcp/dcp-client/compute-groups */ \"./src/dcp-client/compute-groups/index.js\");\nconst RingBuffer = __webpack_require__(/*! dcp/utils/ringBuffer */ \"./src/utils/ringBuffer.js\");\nconst supervisorTuning = dcpConfig.future('worker.tuning');\nconst tuning = {\n watchdogInterval: 7, /**< seconds - time between fetches when ENOTASK(? /wg nov 2019) */\n minSandboxStartDelay: 0.1, /**< seconds - minimum time between WebWorker starts */\n maxSandboxStartDelay: 0.7, /**< seconds - maximum delay time between WebWorker starts */\n ...supervisorTuning\n};\n\n/** Make timers 10x slower when running in niim */\nlet timeDilation = 1;\nif (DCP_ENV.platform === 'nodejs') {\n /** Make timers 10x slower when running in niim */\n timeDilation = (requireNative('module')._cache.niim instanceof requireNative('module').Module) ? 10 : 1;\n}\n\ndcpConfig.future('worker.sandbox', { progressReportInterval: (5 * 60 * 1000) });\nconst sandboxTuning = dcpConfig.worker.sandbox;\n\n/**\n * @typedef {*} address\n * @typedef {*} opaqueId\n */\n\n/**\n * @typedef {object} SandboxSlice\n * @property {Sandbox} sandbox\n * @property {Slice} slice\n */\n\n/**\n * @typedef {object} Signature\n * @property {Uint8Array} r\n * @property {Uint8Array} s\n * @property {Uint8Array} v\n */\n\n/**\n * @typedef {object} SignedAuthorizationMessageObject\n * @property {object} auth\n * @property {Signature} signature\n * @property {module:dcp/wallet.Address} owner\n */\n\n/** @typedef {import('.').Worker} Worker */\n/** @typedef {import('.').SupervisorOptions} SupervisorOptions */\n\nclass Supervisor extends EventEmitter {\n /**\n * @constructor\n * @param {Worker} worker\n * @param {SupervisorOptions} options\n */\n constructor (worker, options={}) {\n super('Supervisor');\n\n /** @type {Worker} */\n this.worker = worker;\n\n /** @type {Sandbox[]} */\n this.sandboxes = [];\n\n /** @type {Sandbox[]} */\n this.readiedSandboxes = [];\n\n /** @type {Sandbox[]} */\n this.assignedSandboxes = [];\n\n /** @type {Slice[]} */\n this.slices = [];\n\n /** @type {Slice[]} */\n this.queuedSlices = [];\n\n /** @type {Slice[]} */\n this.lostSlices = [];\n\n /** @type {boolean} */\n this.matching = false;\n\n /** @type {boolean} */\n this.isFetchingNewWork = false;\n\n /** @type {number} */\n this.numberOfCoresReserved = 0;\n\n /** @type {number} */\n this.addressTruncationLength = 20; // Set to -1 for no truncation.\n\n /** @type {Object[]} */\n this.rejectedJobs = [];\n this.rejectedJobReasons = [];\n\n if (!options) {\n console.error('Supervisor Options', options, new Error().stack);\n options = {};\n }\n\n /** @type {object} */\n this.options = {\n jobAddresses: options.jobAddresses || [/* all jobs unless priorityOnly */],\n ...options,\n };\n\n const { paymentAddress, identityKeystore } = options;\n if (paymentAddress) {\n if (paymentAddress instanceof wallet.Keystore) {\n this.paymentAddress = paymentAddress.address;\n } else {\n this.paymentAddress = new wallet.Address(paymentAddress);\n }\n } else {\n this.paymentAddress = null;\n }\n\n this._identityKeystore = identityKeystore;\n\n // In localExec, do not allow work function or arguments to come from the 'any' origins\n this.allowedOrigins = []\n if (this.options.localExec)\n {\n dcpConfig.worker.allowOrigins.fetchData = dcpConfig.worker.allowOrigins.fetchData.concat(dcpConfig.worker.allowOrigins.any)\n dcpConfig.worker.allowOrigins.sendResults = dcpConfig.worker.allowOrigins.sendResults.concat(dcpConfig.worker.allowOrigins.any)\n }\n else\n this.allowedOrigins = dcpConfig.worker.allowOrigins.any;\n\n if(options.allowedOrigins && options.allowedOrigins.length > 0)\n this.allowedOrigins = options.allowedOrigins.concat(this.allowedOrigins);\n\n /**\n * Maximum sandboxes allowed to work at a given time.\n * @type {number}\n */\n this.maxWorkingSandboxes = options.maxWorkingSandboxes || 1;\n\n /** @type {number} */\n this.defaultMaxGPUs = 1;\n // this.GPUsAssigned = 0;\n \n // Object.defineProperty(this, 'GPUsAssigned', {\n // get: () => this.allocatedSandboxes.filter(sb => !!sb.requiresGPU).length,\n // enumerable: true,\n // configurable: false,\n // });\n\n /**\n * TODO: Remove this when the supervisor sends all of the sandbox\n * capabilities to the scheduler when fetching work.\n * @type {object}\n */\n this.capabilities = null;\n\n /** @type {number} */\n this.lastProgressReport = 0;\n\n /** \n * An N-slot ring buffer of job addresses. Stores all jobs that have had no more than 1 slice run in the ring buffer.\n * Required for the implementation of discrete jobs \n * @type {RingBuffer} \n */\n this.ringBufferofJobs = new RingBuffer(200); // N = 200 should be more than enough.\n \n // @hack - dcp-env.isBrowserPlatform is not set unless the platform is _explicitly_ set,\n // using the default detected platform doesn't set it.\n // Fixing that causes an error in the wallet module's startup on web platform, which I\n // probably can't fix in a reasonable time this morning.\n // ~ER2020-02-20\n\n if (!options.maxWorkingSandboxes\n && DCP_ENV.browserPlatformList.includes(DCP_ENV.platform)\n && navigator.hardwareConcurrency > 1) {\n this.maxWorkingSandboxes = navigator.hardwareConcurrency - 1;\n if (typeof navigator.userAgent === 'string') {\n if (/(Android).*(Chrome|Chromium)/.exec(navigator.userAgent)) {\n this.maxWorkingSandboxes = 1;\n console.log('Doing work with Chromimum browsers on Android is currently limited to one sandbox');\n }\n }\n }\n\n /** @type {SupervisorCache} */\n this.cache = new SupervisorCache(this);\n /** @type {object} */\n this._connections = {}; /* active DCPv4 connections */\n // Call the watchdog every 7 seconds.\n this.watchdogInterval = setInterval(() => this.watchdog(), tuning.watchdogInterval * 1000);\n if (DCP_ENV.platform === 'nodejs' && this.options.localExec) /* interval helps keep normal worker alive forever, which we don't want in localexec */\n this.watchdogInterval.unref();\n\n const ceci = this;\n\n // Initialize to null so these properties are recognized for the Supervisor class\n this.taskDistributorConnection = null;\n this.eventRouterConnection = null;\n this.resultSubmitterConnection = null;\n this.packageManagerConnection = null;\n this.openTaskDistributorConn = function openTaskDistributorConn()\n {\n let config = dcpConfig.scheduler.services.taskDistributor;\n ceci.taskDistributorConnection = new protocolV4.Connection(config, ceci.identityKeystore, connectionOptions(config.location, 'taskDistributor'));\n ceci.taskDistributorConnection.on('close', ceci.openTaskDistributorConn);\n }\n\n this.openEventRouterConn = function openEventRouterConn()\n {\n let config = dcpConfig.scheduler.services.eventRouter;\n ceci.eventRouterConnection = new protocolV4.Connection(config, ceci.identityKeystore, connectionOptions(config.location, 'eventRouter'));\n ceci.eventRouterConnection.on('close', ceci.openEventRouterConn);\n if (ceci.eventRouterMessageQueue.length)\n ceci.resendRejectedMessages(ceci.eventRouterConnection, ceci.eventRouterMessageQueue);\n }\n this.eventRouterMessageQueue = [];\n \n this.openResultSubmitterConn = function openResultSubmitterConn()\n {\n let config = dcpConfig.scheduler.services.resultSubmitter;\n ceci.resultSubmitterConnection = new protocolV4.Connection(config, ceci.identityKeystore, connectionOptions(config.location, 'resultSubmitter'));\n ceci.resultSubmitterConnection.on('close', ceci.openResultSubmitterConn);\n if (ceci.resultSubmitterMessageQueue.length)\n ceci.resendRejectedMessages(ceci.resultSubmitterConnection, ceci.resultSubmitterMessageQueue);\n }\n this.resultSubmitterMessageQueue = [];\n\n this.openPackageManagerConn = function openPackageManagerConn()\n {\n let config = dcpConfig.packageManager;\n ceci.packageManagerConnection = new protocolV4.Connection(config, ceci.identityKeystore, connectionOptions(config.location, 'packageManager'));\n ceci.packageManagerConnection.on('close', ceci.openPackageManagerConn);\n if (ceci.packageManagerMessageQueue.length)\n ceci.resendRejectedMessages(ceci.packageManagerConnection, ceci.packageManagerMessageQueue);\n }\n this.packageManagerMessageQueue = [];\n }\n\n /**\n * Return worker opaqueId.\n * @type {opaqueId}\n */\n get workerOpaqueId() {\n if (!this._workerOpaqueId)\n this._workerOpaqueId = localStorage.getItem('workerOpaqueId');\n\n if (!this._workerOpaqueId || this._workerOpaqueId.length !== constants.workerIdLength) {\n this._workerOpaqueId = generateOpaqueId();\n localStorage.setItem('workerOpaqueId', this._workerOpaqueId);\n }\n\n return this._workerOpaqueId;\n }\n\n /**\n * This getter is the absolute source-of-truth for what the\n * identity keystore is for this instance of the Supervisor.\n */\n get identityKeystore() {\n assert(this.defaultIdentityKeystore);\n\n return this._identityKeystore || this.defaultIdentityKeystore;\n }\n\n /**\n * Open all connections. Used when supervisor is instantiated or stopped/started\n * to initially open connections.\n */\n instantiateAllConnections() {\n if (!this.taskDistributorConnection)\n this.openTaskDistributorConn();\n \n if (!this.eventRouterConnection)\n this.openEventRouterConn();\n \n if (!this.resultSubmitterConnection)\n this.openResultSubmitterConn();\n\n if (!this.packageManagerConnection)\n this.openPackageManagerConn();\n }\n \n /**\n * Try sending messages that were rejected on an old instance of the given connection.\n */\n async resendRejectedMessages(connection, messageQueue) {\n var message = messageQueue.shift();\n\n do {\n try {\n await connection.send(message.operation, message.data);\n } catch (e) {\n debugging('supervisor') && console.error(`Failed to send message ${message.operation} to scheduler: ${e}. Will try again on a new connection.`);\n messageQueue.unshift(message);\n connection.close();\n break;\n }\n message = messageQueue.shift();\n } while (message)\n }\n\n /** Set the default identity keystore -- needs to happen before anything that talks\n * to the scheduler for work gets called. This is a wart and should be removed by\n * refactoring.\n *\n * The default identity keystore will be used if the Supervisor was not provided\n * with an alternate. This keystore will be located via the Wallet API, and \n * if not found, a randomized default identity will be generated. \n *\n * @param {object} ks An instance of wallet::Keystore -- if undefined, we pick the best default we can.\n * @returns {Promise<void>}\n */\n async setDefaultIdentityKeystore(ks) {\n try {\n if (ks) {\n this.defaultIdentityKeystore = ks;\n return;\n }\n\n if (this.defaultIdentityKeystore)\n return;\n\n try {\n this.defaultIdentityKeystore = await wallet.getId();\n } catch(e) {\n debugging('supervisor') && console.debug('Error generating default identity, try to do it another way.');\n this.defaultIdentityKeystore = await new wallet.IdKeystore(null, '');\n }\n } finally {\n if (this.defaultIdentityKeystore)\n debugging('supervisor') && console.debug('Set default identity =', this.defaultIdentityKeystore.address);\n else\n debugging('supervisor') && console.debug('Failed to set default identity, worker cannot work.');\n }\n }\n\n //\n // What follows is a bunch of utility properties and functions for creating filtered views\n // of the slices and sandboxes array.\n //\n /** XXXpfr @todo Write sort w/o using promises so we can get rid of async on all the compress functions. */\n\n /**\n * @deprecated -- Please do not use this.workingSandboxes; use this.allocatedSandboxes instead.\n * Sandboxes that are in WORKING state.\n *\n * Warning: Do not rely on this information being 100% accurate -- it may change in the next instant.\n * @type {Sandbox[]}\n */\n get workingSandboxes() {\n return this.sandboxes.filter(sandbox => sandbox.isWorking);\n }\n\n /**\n * Use instead of this.workingSandboxes.\n *\n * When a sandbox is paired with a slice, execution is pending and sandbox.allocated=true and\n * sandbox.slice=slice and sandbox.jobAddress=slice.jobAddress. This is what 'allocated' means.\n * Immediately upon the exit of sandbox.work, sandbox.allocated=false is set and if an exception\n * wasn't thrown the sandbox is placed in this.assignedSandboxes.\n * Thus from the pov of supervisor, this.allocatedSandboxes is deterministic and this.workingSandboxes is not.\n * Please try to not use this.workingSandboxes. It is deprecated.\n *\n * Warning: Do not rely on this information being 100% accurate -- it may change in the next instant.\n * @type {Sandbox[]}\n */\n get allocatedSandboxes() {\n return this.sandboxes.filter(sandbox => sandbox.allocated);\n }\n\n /**\n * Slices that are allocated.\n * Warning: Do not rely on this information being 100% accurate -- it may change in the next instant.\n * @type {Slice[]}\n */\n get allocatedSlices() {\n return this.slices.filter(slice => slice.allocated);\n }\n\n /**\n * This property is used as the target number of sandboxes to be associated with slices and start working.\n *\n * It is used in this.watchdog as to prevent a call to this.work when unallocatedSpace <= 0.\n * It is also used in this.distributeQueuedSlices where it is passed as an argument to this.matchSlicesWithSandboxes to indicate how many sandboxes\n * to associate with slices and start working.\n *\n * Warning: Do not rely on this information being 100% accurate -- it may change in the next instant.\n * @type {number}\n */\n get unallocatedSpace() {\n return this.maxWorkingSandboxes - this.allocatedSandboxes.length - this.numberOfCoresReserved;\n }\n \n /**\n * Call acquire(numberOfCoresToReserve) to reserve numberOfCoresToReserve unallocated sandboxes as measured by unallocatedSpace.\n * Call release() to undo the previous acquire.\n * This pseudo-mutex technique helps prevent races in scheduling slices in Supervisor.\n * @param {number} numberOfCoresToReserve\n */\n acquire(numberOfCoresToReserve) { \n this.numberOfCoresReserved = numberOfCoresToReserve; \n }\n release() { \n this.numberOfCoresReserved = 0; \n }\n\n /**\n * Remove from this.slices.\n * @param {Slice} slice\n */\n removeSlice(slice) {\n this.removeElement(this.slices, slice);\n if (Supervisor.debugBuild) {\n if (this.queuedSlices.indexOf(slice) !== -1)\n throw new Error(`removeSlice: slice ${slice.identifier} is in queuedSlices; inconsistent state.`);\n if (this.lostSlices.length > 0) {\n console.warn(`removeSlice: slice ${slice.identifier}, found lostSlices ${this.lostSlices.map(s => s.identifier)}`);\n if (this.lostSlices.indexOf(slice) !== -1)\n throw new Error(`removeSlice: slice ${slice.identifier} is in lostSlices; inconsistent state.`);\n }\n }\n }\n\n /**\n * Remove from this.slices.\n * @param {Slice[]} slices\n */\n removeSlices(slices) {\n this.slices = this.slices.filter(slice => slices.indexOf(slice) === -1);\n }\n\n /**\n * Remove from this.queuedSlices.\n * @param {Slice[]} slices\n */\n removeQueuedSlices(slices) {\n this.queuedSlices = this.queuedSlices.filter(slice => slices.indexOf(slice) === -1);\n }\n\n /**\n * Remove from this.sandboxes, this.assignedSandboxes and this.readiedSandboxes.\n * @param {Sandbox} sandbox\n */\n removeSandbox(sandbox) {\n debugging('scheduler') && console.log(`removeSandbox ${sandbox.identifier}`);\n this.removeElement(this.sandboxes, sandbox);\n this.removeElement(this.assignedSandboxes, sandbox);\n\n // XXXpfr: April 13, 2022\n // I'm trying to understand and control when sandboxes get removed.\n // A sandbox in this.readiedSandboxes should never have returnSandbox/removeSandbox called on it except in stopWork.\n // Because of races and random worker crashes, it is hard to get this right, but I want to try.\n // If I don't fix this is the next 30 days or I forget, please delete this exception.\n if (false)\n {}\n\n this.removeElement(this.readiedSandboxes, sandbox);\n }\n\n /**\n * Remove from this.sandboxes and this.assignedSandboxes .\n * @param {Sandbox[]} sandboxes\n */\n async removeSandboxes(sandboxes) {\n debugging('scheduler') && console.log(`removeSandboxes: Remove ${sandboxes.length} sandboxes ${this.dumpSandboxes(sandboxes)}`);\n this.sandboxes = this.sandboxes.filter(sandbox => sandboxes.indexOf(sandbox) === -1);\n this.assignedSandboxes = this.assignedSandboxes.filter(sandbox => sandboxes.indexOf(sandbox) === -1);\n\n if (Supervisor.debugBuild) {\n const readied = this.readiedSandboxes.filter(sandbox => sandboxes.indexOf(sandbox) !== -1);\n if (readied.length > 0)\n throw new Error(`removeSandboxes: sandboxes ${readied.map(s => s.identifier)} are in readiedSandboxes; inconsistent state.`);\n }\n }\n\n /**\n * Remove element from theArray.\n * @param {Array<*>} theArray\n * @param {object|number} element\n * @param {boolean} [assertExists = true]\n */\n removeElement(theArray, element, assertExists = false) {\n let index = theArray.indexOf(element);\n assert(index !== -1 || !assertExists);\n if (index !== -1) theArray.splice(index, 1);\n }\n\n /**\n * Log sliceArray.\n * @param {Slice[]} sliceArray\n * @param {string} [header]\n * @returns {string}\n */\n dumpSlices(sliceArray, header) {\n if (header) console.log(`\\n${header}`);\n return compressSlices(sliceArray, this.addressTruncationLength);\n }\n\n /**\n * Log sandboxArray.\n * @param {Sandbox[]} sandboxArray\n * @param {string} [header]\n * @returns {string}\n */\n dumpSandboxes(sandboxArray, header) {\n if (header) console.log(`\\n${header}`);\n return compressSandboxes(sandboxArray, this.addressTruncationLength);\n }\n\n /**\n * If the elements of sandboxSliceArray are not unique, log the duplicates and dump the array.\n * @param {SandboxSlice[]} sandboxSliceArray\n * @param {string} header\n */\n dumpSandboxSlicesIfNotUnique(sandboxSliceArray, header) {\n if (!this.isUniqueSandboxSlices(sandboxSliceArray, header))\n console.log(this.dumpSandboxSlices(sandboxSliceArray));\n }\n\n /**\n * Log { sandbox, slice }.\n * @param {Sandbox} sandbox\n * @param {Slice} slice\n * @returns {string}\n */\n dumpSandboxAndSlice(sandbox, slice) {\n return `${sandbox.id}~${slice.sliceNumber}.${this.dumpJobAddress(slice.jobAddress)}`;\n }\n\n /**\n * Log { sandbox, slice } with state/status.\n * @param {Sandbox} sandbox\n * @param {Slice} slice\n * @returns {string}\n */\n dumpStatefulSandboxAndSlice(sandbox, slice) {\n return `${sandbox.id}.${sandbox.state}~${slice.sliceNumber}.${this.dumpJobAddress(slice.jobAddress)}.${slice.status}`;\n }\n\n /**\n * Truncates jobAddress.toString() to this.addressTruncationLength digits.\n * @param {address} jobAddress\n * @returns {string}\n */\n dumpJobAddress(jobAddress) {\n return truncateAddress(jobAddress, this.addressTruncationLength /* digits*/);\n }\n\n /**\n * Dump sandboxSliceArray.\n * @param {SandboxSlice[]} sandboxSliceArray - input array of { sandbox, slice }\n * @param {string} [header] - optional header\n * @param {boolean} [stateFul] - when true, also includes slice.status and sandbox.state.\n * @returns {string}\n */\n dumpSandboxSlices(sandboxSliceArray, header, stateFul=false) {\n if (header) console.log(`\\n${header}`);\n const jobMap = {};\n sandboxSliceArray.forEach(ss => {\n const sss = stateFul ? `${ss.sandbox.id}.${ss.sandbox.state}~${ss.slice.sliceNumber}.${ss.slice.status}` : `${ss.sandbox.id}~${ss.slice.sliceNumber}`;\n if (!jobMap[ss.slice.jobAddress]) jobMap[ss.slice.jobAddress] = sss;\n else jobMap[ss.slice.jobAddress] += `,${sss}`;\n });\n let output = '';\n for (const [jobAddress, sss] of Object.entries(jobMap))\n output += `${this.dumpJobAddress(jobAddress)}:[${sss}]:`;\n return output;\n }\n\n /**\n * Check sandboxSliceArray for duplicates.\n * @param {SandboxSlice[]} sandboxSliceArray\n * @param {string} [header]\n * @param {function} [log]\n * @returns {boolean}\n */\n isUniqueSandboxSlices(sandboxSliceArray, header, log) {\n const result = [], slices = [], sandboxes = [];\n let once = true;\n sandboxSliceArray.forEach(x => {\n const sliceIndex = slices.indexOf(x.slice);\n const sandboxIndex = sandboxes.indexOf(x.sandbox);\n\n if (sandboxIndex >= 0) {\n if (once && header) console.log(`\\n${header}`); once = false;\n log ? log(x.sandbox) : console.log(`\\tWarning: Found duplicate sandbox ${x.sandbox.identifier}.`);\n } else sandboxes.push(x.sandbox);\n\n if (sliceIndex >= 0) {\n if (once && header) console.log(`\\n${header}`); once = false;\n log ? log(x.slice) : console.log(`\\tWarning: Found duplicate slice ${x.slice.identifier}.`);\n } else {\n slices.push(x.slice);\n if (sandboxIndex < 0) result.push(x);\n }\n });\n return sandboxSliceArray.length === result.length;\n }\n\n /**\n * Attempts to create and start a given number of sandboxes.\n * The sandboxes that are created can then be assigned for a\n * specific job at a later time. All created sandboxes\n * get put into the @this.readiedSandboxes array when allocateLocalSandboxes is false.\n *\n * @param {number} numSandboxes - the number of sandboxes to create\n * @param {boolean} [allocateLocalSandboxes=false] - when true, do not place in this.readiedSandboxes\n * @returns {Promise<Sandbox[]>} - resolves with array of created sandboxes, rejects otherwise\n * @throws when given a numSandboxes is not a number or if numSandboxes is Infinity\n */\n async readySandboxes (numSandboxes, allocateLocalSandboxes = false) {\n debugging('supervisor') && console.debug(`readySandboxes: Readying ${numSandboxes} sandboxes, total sandboxes ${this.sandboxes.length}, matching ${this.matching}, fetching ${this.isFetchingNewWork}`);\n \n if (typeof numSandboxes !== 'number' || Number.isNaN(numSandboxes) || numSandboxes === Infinity) {\n throw new Error(`${numSandboxes} is not a number of sandboxes that can be readied.`);\n }\n if (numSandboxes <= 0) {\n return [];\n }\n\n const sandboxStartPromises = [];\n const sandboxes = [];\n const errors = [];\n for (let i = 0; i < numSandboxes; i++) {\n const sandbox = new Sandbox(this.cache, {\n ...this.options.sandboxOptions,\n }, this.allowedOrigins);\n sandbox.addListener('ready', () => this.emit('sandboxReady', sandbox));\n sandbox.addListener('start', () => {\n this.emit('sandboxStart', sandbox);\n\n // When sliceNumber == 0, result-submitter status skips the slice,\n // so don't send it in the first place.\n // The 'start' event is fired when a worker starts up, hence there's no way\n // to determine whether sandbox has a valid slice without checking.\n if (sandbox.slice) {\n const jobAddress = sandbox.jobAddress;\n const sliceNumber = sandbox.slice.sliceNumber;\n // !authorizationMessage <==> sliceNumber === 0.\n const authorizationMessage = sandbox.slice.getAuthorizationMessage();\n\n if (authorizationMessage) {\n let statusPayload = {\n worker: this.workerOpaqueId,\n slices: [{\n job: jobAddress,\n sliceNumber: sliceNumber,\n status: 'begin',\n authorizationMessage,\n }],\n }\n \n try /* resultSubmitterConnection can be null if worker is stopped */\n {\n this.resultSubmitterConnection.send('status', statusPayload).catch((error) => {\n debugging('supervisor') && console.error(`Error sending 'status' for slice ${sliceNumber} of job ${jobAddress}:\\n ${error}\\nWill try again on a new connection`);\n this.resultSubmitterMessageQueue.push({ operation: 'status', data: statusPayload });\n this.resultSubmitterConnection.close();\n });\n } catch (error)\n {\n debugging('supervisor') && console.error(`Failed to send 'status' for slice ${sliceNumber} of job ${jobAddress}:, no connection to result submitter:`, error);\n }\n }\n }\n });\n sandbox.addListener('workEmit', ({ eventName, payload }) => {\n // Need to check if the sandbox hasn't been assigned a slice yet.\n if (!sandbox.slice) {\n if (Supervisor.debugBuild) {\n console.error(\n `Sandbox not assigned a slice before sending workEmit message to scheduler. 'workEmit' event originates from \"${eventName}\" event`, \n payload,\n );\n }\n }\n else\n {\n const jobAddress = sandbox.slice.jobAddress;\n const sliceNumber = sandbox.slice.sliceNumber;\n // sliceNumber can be zero if it came from a problem with loading modules.\n assert(jobAddress && (sliceNumber || sliceNumber === 0));\n // Send a work emit message from the sandbox to the event router\n // !authorizationMessage <==> sliceNumber === 0.\n let authorizationMessage;\n try {\n // Sometimes a sliceNumber===0 workEmit comes in before the client bundle is properly loaded.\n // Also happens with minor dcp-client version mismatches.\n authorizationMessage = sandbox.slice.getAuthorizationMessage();\n } catch(e) {\n authorizationMessage = null;\n }\n\n if (!authorizationMessage)\n {\n console.warn(`workEmit: missing authorization message for job ${jobAddress}, slice: ${sliceNumber}`);\n return Promise.resolve();\n }\n \n let workEmitPayload = {\n eventName,\n payload,\n job: jobAddress,\n slice: sliceNumber,\n worker: this.workerOpaqueId,\n authorizationMessage,\n }\n \n const workEmitPromise = this.eventRouterConnection.send('workEmit', workEmitPayload).catch(error => {\n debugging('supervisor') && console.warn(`workEmit: unable to send ${eventName} for slice ${sliceNumber} of job ${jobAddress}: ${error.message}.\\nTrying again on a new connection.`);\n this.eventRouterMessageQueue.push({ operation: 'workEmit', data: workEmitPayload })\n this.eventRouterConnection.close();\n if (Supervisor.debugBuild)\n console.error('workEmit error:', error);\n });\n\n if (Supervisor.debugBuild) {\n workEmitPromise.then(result => {\n if (!result || !result.success)\n console.warn('workEmit: event router did not accept event', result);\n });\n }\n }\n });\n\n // When any sbx completes, \n sandbox.addListener('complete', () => {\n this.watchdog();\n });\n\n sandbox.on('sandboxError', (error) => handleSandboxError(this, sandbox, error));\n \n sandbox.on('rejectedWorkMetrics', (data) =>{\n function updateRejectedMetrics(report) {\n ['total', 'CPU', 'webGL'].forEach((key) => {\n if (report[key]) sandbox.slice.rejectedTimeReport[key] += report[key];\n })\n }\n \n // If the slice already has rejected metrics, add this data to it. If not, assign this data to slices rejected metrics property\n if (sandbox.slice) {\n (sandbox.slice.rejectedTimeReport) ? updateRejectedMetrics(data.timeReport) : sandbox.slice.rejectedTimeReport = data.timeReport;\n }\n })\n \n // If the sandbox terminated and we are not shutting down, then should return all work which is currently\n // not being computed if all sandboxes are dead and the attempt to create a new one fails.\n sandbox.on('terminated',async () => {\n if (this.sandboxes.length > 0) {\n let terminatedSandboxes = this.sandboxes.filter(sbx => sbx.isTerminated);\n if (terminatedSandboxes.length === this.sandboxes.length) {\n debugging('supervisor') && console.debug(`readySandboxes: Create 1 sandbox in the sandbox-terminated-handler, total sandboxes ${this.sandboxes.length}, matching ${this.matching}, fetching ${this.isFetchingNewWork}`);\n await this.readySandboxes(1);\n \n // If we cannot create a new sandbox, that probably means we're on a screensaver worker\n // and the screensaver is down. So return the slices to the scheduler.\n if (this.sandboxes.length !== terminatedSandboxes.length + 1) {\n this.returnSlices(this.queuedSlices).then(() => {\n this.queuedSlices.length = 0;\n });\n }\n }\n }\n })\n\n const delayMs =\n 1000 *\n (tuning.minSandboxStartDelay +\n Math.random() *\n (tuning.maxSandboxStartDelay - tuning.minSandboxStartDelay));\n \n sandboxStartPromises.push(\n sandbox\n .start(delayMs)\n .then(() => {\n if (!allocateLocalSandboxes) this.readiedSandboxes.push(sandbox);\n this.sandboxes.push(sandbox);\n sandboxes.push(sandbox);\n }).catch((err) => {\n errors.push(err);\n this.returnSandbox(sandbox);\n if (err.code === 'ENOWORKER') {\n throw new DCPError(\"Cannot use localExec without dcp-worker installed. Use the command 'npm install dcp-worker' to install the neccessary modules.\", 'ENOWORKER');\n }\n }));\n }\n \n await Promise.all(sandboxStartPromises);\n\n if (errors.length) {\n console.warn(`Failed to ready ${errors.length} of ${numSandboxes} sandboxes.`, errors);\n throw new Error('Failed to ready sandboxes.');\n }\n\n debugging('supervisor') && console.log(`readySandboxes: Readied ${sandboxes.length} sandboxes ${JSON.stringify(sandboxes.map(sandbox => sandbox.id))}`);\n \n return sandboxes;\n }\n\n /**\n * Accepts a sandbox after it has finished working or encounters an error.\n * If the sandbox was terminated or if \"!slice || slice.failed\" then\n * the sandbox will be removed from the sandboxes array and terminated if necessary.\n * Otherwise it will try to distribute a slice to the sandbox immediately.\n *\n * @param {Sandbox} sandbox - the sandbox to return\n * @param {Slice} [slice] - the slice just worked on; !slice => terminate\n * @param {boolean} [verifySandboxIsNotTerminated=true] - if true, check sandbox is not already terminated\n */\n returnSandbox (sandbox, slice, verifySandboxIsNotTerminated=true) {\n if (!slice || slice.failed || sandbox.isTerminated) {\n \n this.removeSandbox(sandbox);\n \n if (!sandbox.isTerminated) {\n debugging('supervisor') && console.log(`Supervisor.returnSandbox: Terminating ${sandbox.identifier}${slice ? `~${slice.identifier}` : ''}, # of sandboxes ${this.sandboxes.length}`);\n sandbox.terminate(false);\n } else {\n debugging('supervisor') && console.log(`Supervisor.returnSandbox: Already terminated ${sandbox.identifier}${slice ? `~${slice.identifier}` : ''}, # of sandboxes ${this.sandboxes.length}`);\n // XXXpfr: April 13, 2022\n // I'm trying to understand and control when sandboxes get terminated.\n // Because of races and random worker crashes, it is impossible to not try to terminate a sandbox more than once.\n // But at some places where returnSandbox is we shouldn't see this behavior, hence this exception.\n // If I don't fix this is the next 30 days or I forget, please delete this exception.\n if (false)\n {}\n }\n }\n }\n\n /**\n * Terminates sandboxes, in order of creation, when the total started sandboxes exceeds the total allowed sandboxes.\n *\n * @returns {Promise<void>}\n */\n pruneSandboxes () {\n let numOver = this.sandboxes.length - (dcpConfig.worker.maxAllowedSandboxes + this.maxWorkingSandboxes);\n if (numOver <= 0) return;\n \n // Don't kill readied sandboxes while creating readied sandboxes.\n for (let index = 0; index < this.readiedSandboxes.length; ) {\n const sandbox = this.readiedSandboxes[index];\n // If the sandbox is allocated, advance to the next one in the list.\n if (sandbox.allocated) {\n index++;\n continue;\n }\n // Otherwise, remove this sandbox but look at the same array index in the next loop.\n debugging('supervisor') && console.log(`pruneSandboxes: Terminating readied sandbox ${sandbox.identifier}`);\n this.readiedSandboxes.splice(index, 1);\n this.returnSandbox(sandbox);\n\n if (--numOver <= 0) break;\n }\n\n if (numOver <= 0) return;\n for (let index = 0; index < this.assignedSandboxes.length; ) {\n const sandbox = this.assignedSandboxes[index];\n // If the sandbox is allocated, advance to the next one in the list.\n if (sandbox.allocated) {\n index++;\n continue;\n }\n // Otherwise, remove this sandbox but look at the same array index in the next loop.\n debugging('supervisor') && console.log(`pruneSandboxes: Terminating assigned sandbox ${sandbox.identifier}`);\n this.assignedSandboxes.splice(index, 1);\n this.returnSandbox(sandbox);\n\n if (--numOver <= 0) break;\n }\n }\n \n /**\n * Basic watch dog to check if there are idle sandboxes and\n * attempts to nudge the supervisor to feed them work.\n *\n * Run in an interval created in @constructor .\n * @returns {Promise<void>}\n */\n async watchdog () {\n if (!this.watchdogState)\n this.watchdogState = {};\n\n // Every 5 minutes, report progress of all working slices to the scheduler\n if (Date.now() > ((this.lastProgressReport || 0) + sandboxTuning.progressReportInterval)) {\n // console.log('454: Assembling progress update...');\n this.lastProgressReport = Date.now();\n\n //\n // Note: this.slices is the disjoint union of:\n // this.allocatedSlices, \n // this.queuedSlices, \n // this.slices.filter(slice => !slice.isUnassigned) .\n // When a slice is not in these 3 arrays, the slice is lost.\n //\n \n const currentLostSlices = this.slices.filter(slice => slice.isUnassigned \n && this.queuedSlices.indexOf(slice) === -1\n && this.allocatedSlices.indexOf(slice) === -1);\n\n if (currentLostSlices.length > 0) {\n this.lostSlices.push(...currentLostSlices);\n // Try to recover.\n // Needs more work and testing.\n // Test when we can come up with a decent lost slice repro case.\n // --> this.queuedSlices.push(...currentLostSlices);\n }\n\n if (this.lostSlices.length > 0) {\n if (true) { // Keep this on for awhile, until we know lost slices aren't happening.\n console.warn('Supervisor.watchdog: Found lost slices!');\n for (const slice of this.lostSlices)\n console.warn('\\t', slice.identifier);\n }\n this.lostSlices = this.lostSlices.filter(slice => slice.isUnassigned);\n }\n\n const slices = [];\n this.queuedSlices.forEach(slice => {\n assert(slice && slice.sliceNumber > 0);\n addToSlicePayload(slices, slice, sliceStatus.scheduled);\n });\n\n this.allocatedSlices.forEach(slice => {\n assert(slice && slice.sliceNumber > 0);\n addToSlicePayload(slices, slice, 'progress'); // Beacon.\n });\n\n if (slices.length) {\n // console.log('471: sending progress update...');\n const progressReportPayload = {\n worker: this.workerOpaqueId,\n slices,\n };\n\n this.resultSubmitterConnection.send('status', progressReportPayload)\n .catch(error => {\n debugging('supervisor') && console.error('479: Failed to send status update:', error/*.message*/);\n this.resultSubmitterMessageQueue.push({ operation: 'status', data: progressReportPayload })\n this.resultSubmitterConnection.close();\n });\n }\n }\n\n if (this.worker.working) {\n if (this.unallocatedSpace > 0) {\n await this.work().catch(err => {\n if (!this.watchdogState[err.code || '0'])\n this.watchdogState[err.code || '0'] = 0;\n if (Date.now() - this.watchdogState[err.code || '0'] > ((dcpConfig.worker.watchdogLogInterval * timeDilation || 120) * 1000))\n console.error('301: Failed to start work:', err);\n this.watchdogState[err.code || '0'] = Date.now();\n });\n }\n\n this.pruneSandboxes();\n }\n }\n\n /**\n * Gets the logical and physical number of cores and also\n * the total number of sandboxes the worker is allowed to run\n *\n */\n getStatisticsCPU() {\n if (DCP_ENV.isBrowserPlatform) {\n return {\n worker: this.workerOpaqueId,\n lCores: window.navigator.hardwareConcurrency,\n pCores: dcpConfig.worker.pCores || window.navigator.hardwareConcurrency,\n sandbox: this.maxWorkingSandboxes\n }\n }\n\n return {\n worker: this.workerOpaqueId,\n lCores: requireNative('os').cpus().length,\n pCores: requireNative('physical-cpu-count'),\n sandbox: this.maxWorkingSandboxes\n }\n }\n\n /**\n * Returns the number of unallocated sandbox slots to send to fetchTask.\n *\n * @returns {number}\n */\n numberOfAvailableSandboxSlots() {\n let numCores;\n if (this.options.priorityOnly && this.options.jobAddresses.length === 0) {\n numCores = 0;\n } else if (this.queuedSlices.length > 1) {\n // We have slices queued, no need to fetch\n numCores = 0;\n } else {\n // The queue is almost empty (there may be 0 or 1 element), fetch a full task.\n // The task is full, in the sense that it will contain slices whose\n // aggregate execution time is this.maxWorkingSandboxes * 5-minutes.\n // However, there can only be this.unallocatedSpace # of long slices.\n // Thus we need to know whether the last slice in this.queuedSlices is long or not.\n // (A long slice has estimated execution time >= 5-minutes.)\n const longSliceCount = (this.queuedSlices.length > 0 && this.queuedSlices[0].isLongSlice) ? 1 : 0;\n numCores = this.unallocatedSpace - longSliceCount;\n }\n return numCores;\n }\n\n /**\n * Call to start doing work on the network.\n * This is the one place where requests to fetch new slices are made.\n * After the initial slices are fetched it calls this.distributeQueuedSlices.\n *\n * @returns {Promise<void>}, unallocatedSpace ${this.unallocatedSpace}\n */\n async work()\n {\n // When inside matchSlicesWithSandboxes, don't reenter Supervisor.work to fetch new work or create new sandboxes.\n if (this.matching) {\n // Interesting and noisy.\n // debugging('supervisor') && console.log(`Supervisor.work: Do not interleave work, fetch or matching slices with sandboxes: queuedSlices ${this.queuedSlices.length}, unallocatedSpace ${this.unallocatedSpace}, matching ${this.matching}, fetching ${this.isFetchingNewWork}`);\n return Promise.resolve();\n }\n\n await this.setDefaultIdentityKeystore();\n\n // Instantiate connections that don't exist.\n this.instantiateAllConnections();\n\n const numCores = this.numberOfAvailableSandboxSlots();\n\n debugging() && console.log(`Supervisor.work: Try to get ${numCores} slices in working sandboxes, unallocatedSpace ${this.unallocatedSpace}, queued slices ${this.queuedSlices.length}, # of sandboxes ${this.sandboxes.length}, matching ${this.matching}, fetching: ${this.isFetchingNewWork}`);\n \n // Fetch a new task if we have no more slices queued, then start workers\n try {\n if (numCores > 0 && !this.isFetchingNewWork) {\n this.isFetchingNewWork = true;\n\n /**\n * This will only ready sandboxes up to a total count of\n * maxWorkingSandboxes (in any state). It is not possible to know the\n * actual number of sandboxes required until we have the slices because we\n * may have sandboxes assigned for the slice's job already.\n *\n * If the evaluator cannot start (ie. if the evalServer is not running),\n * then the while loop will keep retrying until the evalServer comes online\n */\n if (this.maxWorkingSandboxes > this.sandboxes.length) {\n // Note: The old technique had \n // while (this.maxWorkingSandboxes > this.sandboxes.length) {....\n // and sometimes we'd get far too many sandboxes, because it would keep looping while waiting for\n // this.readySandboxes(this.maxWorkingSandboxes - this.sandboxes.length);\n // to construct the rest of the sandboxes. The fix is to only loop when the 1st \n // await this.readySandboxes(1) \n // is failing.\n let needFirstSandbox = true;\n while (needFirstSandbox) {\n debugging('supervisor') && console.log(`Supervisor.work: ready 1 sandbox, # of sandboxes ${this.sandboxes.length}, matching ${this.matching}, fetching ${this.isFetchingNewWork}`);\n await this.readySandboxes(1)\n .then(() => {\n debugging('supervisor') && console.log(`Supervisor.work: ready ${this.maxWorkingSandboxes - this.sandboxes.length} sandbox(es), # of sandboxes ${this.sandboxes.length}, matching ${this.matching}, fetching ${this.isFetchingNewWork}`);\n this.readySandboxes(this.maxWorkingSandboxes - this.sandboxes.length);\n needFirstSandbox = false;\n }).catch(error => {\n console.warn('906: failed to ready sandboxes; will retry', error.code, error.message);\n });\n }\n }\n\n /**\n * Temporary change: Assign the capabilities of one of readied sandboxes\n * before fetching slices from the scheduler.\n *\n * TODO: Remove this once fetchTask uses the capabilities of every\n * sandbox to fetch slices.\n */\n if (!this.capabilities) {\n this.capabilities = this.sandboxes[0].capabilities;\n this.emit('capabilitiesCalculated', this.capabilities);\n }\n\n if (DCP_ENV.isBrowserPlatform && this.capabilities.browser)\n this.capabilities.browser.chrome = DCP_ENV.isBrowserChrome;\n\n const fetchTimeout = setTimeout(() => {\n console.warn(`679: Fetch exceeded timeout, will reconnect at next watchdog interval`);\n \n this.taskDistributorConnection.close('Fetch timed out', Math.random() > 0.5).catch(error => {\n console.error(`931: Failed to close task-distributor connection`, error);\n });\n this.resultSubmitterConnection.close('Fetch timed out', Math.random() > 0.5).catch(error => {\n console.error(`920: Failed to close result-submitter connection`, error);\n });\n this.isFetchingNewWork = false;\n this.instantiateAllConnections();\n }, 3 * 60 * 1000); // max out at 3 minutes to fetch\n\n // ensure result submitter connection before fetching tasks\n try\n {\n await this.resultSubmitterConnection.keepalive();\n }\n catch (e)\n {\n console.error('Failed to connect to result submitter, refusing to fetch slices. Will try again at next fetch cycle.')\n debugging('supervisor') && console.log(`Error: ${e}`);\n this.isFetchingNewWork = false; // <-- done in the `finally` block, below\n clearTimeout(fetchTimeout);\n this.taskDistributorConnection.close('Failed to connect to result-submitter', true).catch(error => {\n console.error(`939: Failed to close task-distributor connection`, error);\n });\n this.resultSubmitterConnection.close('Failed to connect to result-submitter', true).catch(error => {\n console.error(`942: Failed to close result-submitter connection`, error);\n });\n return Promise.resolve();\n }\n await this.fetchTask(numCores).finally(() => {\n clearTimeout(fetchTimeout);\n this.isFetchingNewWork = false;\n });\n }\n\n this.distributeQueuedSlices().then(() => debugging('supervisor') && 'supervisor: finished distributeQueuedSlices()').catch((e) => {\n // We should never get here, because distributeQueuedSlices was changed\n // to try to catch everything and return slices and sandboxes.\n // If we do catch here it may mean a slice was lost. \n console.error('Supervisor.work catch handler for distributeQueuedSlices.', e);\n });\n // No catch(), because it will bubble outward to the caller\n } finally {\n }\n }\n\n /**\n * Generate the workerComputeGroups property of the requestTask message. \n * \n * Concatenate the compute groups object from dcpConfig with the list of compute groups\n * from the supervisor, and remove the public group if accidentally present. Finally,\n * we transform joinSecrets/joinHash into joinHashHash for secure transmission.\n *\n * @note computeGroup objects with joinSecrets are mutated to record their hashes. This\n * affects the supervisor options and dcpConfig. Re-adding a joinSecret property\n * to one of these will cause the hash to be recomputed.\n */\n generateWorkerComputeGroups()\n {\n var computeGroups = Object.values(dcpConfig.worker.computeGroups || {});\n if (this.options.computeGroups)\n computeGroups = computeGroups.concat(this.options.computeGroups);\n computeGroups = computeGroups.filter(group => group.id !== constants.computeGroups.public.id);\n const hashedComputeGroups = [];\n for (const group of computeGroups)\n {\n const groupCopy = Object.assign({}, group);\n if ((group.joinSecret || group.joinHash) && (!group.joinHashHash || this.lastDcpsid !== this.taskDistributorConnection.dcpsid))\n {\n let joinHash;\n if (group.joinHash) {\n joinHash = group.joinHash.replace(/\\s+/g, ''); // strip whitespace\n } else {\n joinHash = calculateJoinHash(groupCopy);\n } \n\n groupCopy.joinHashHash = hash.calculate(hash.eh1, joinHash, this.taskDistributorConnection.dcpsid);\n delete groupCopy.joinSecret;\n delete groupCopy.joinHash;\n debugging('computeGroups') && console.debug(`Calculated joinHash=${joinHash} for`, groupCopy);\n }\n hashedComputeGroups.push(groupCopy);\n }\n this.lastDcpsid = this.taskDistributorConnection.dcpsid;\n debugging('computeGroups') && console.debug('Requesting ', computeGroups.length, 'non-public groups for session', this.lastDcpsid);\n return hashedComputeGroups;\n }\n\n /**\n * Remove all unreferenced jobs in `this.cache`.\n *\n * @param {any[]} newJobs - Jobs that should not be removed from\n * `this.cache`.\n */\n cleanJobCache(newJobs = []) {\n /* Delete all jobs in the supervisorCache that are not represented in this newJobs,\n * or in this.queuedSlices, or there is no sandbox assigned to these jobs.\n * Note: There can easily be 200+ places to check; using a lookup structure to maintain O(n).\n */\n if (this.cache.jobs.length > 0) {\n const jobAddressMap = {};\n Object.keys(newJobs).forEach(jobAddress => { jobAddressMap[jobAddress] = 1; });\n this.slices.forEach(slice => { if (!jobAddressMap[slice.jobAddress]) jobAddressMap[slice.jobAddress] = 1; });\n this.cache.jobs.forEach(jobAddress => {\n if (!jobAddressMap[jobAddress]) {\n this.cache.remove('job', jobAddress);\n // Remove and return the corresponding sandboxes from this.sandboxes.\n const deadSandboxes = this.sandboxes.filter(sb => sb.jobAddress === jobAddress);\n if (deadSandboxes.length > 0) {\n deadSandboxes.forEach(sandbox => { this.returnSandbox(sandbox); });\n debugging('supervisor') && console.log(`Supervisor.fetchTask: Deleting job ${jobAddress} from cache and assigned sandboxes ${deadSandboxes.map(s => s.id)}, # of sandboxes ${this.sandboxes.length}.`);\n }\n }\n });\n }\n }\n\n /**\n * Fetches a task, which contains job information and slices for sandboxes and\n * manages events related to fetching tasks so the UI can more clearly display\n * to user what is actually happening.\n * @param {number} numCores\n * @returns {Promise<void>} The requestTask request, resolve on success, rejects otherwise.\n * @emits Supervisor#fetchingTask\n * @emits Supervisor#fetchedTask\n */\n async fetchTask(numCores) {\n\n // Don't reenter\n if (this.matching || numCores <= 0) {\n // Interesting and noisy.\n debugging('supervisor') && console.log(`Supervisor.fetchTask: Do not nest work, fetch or matching slices with sandboxes: queuedSlices ${this.queuedSlices.length}, unallocatedSpace ${this.unallocatedSpace}, matching ${this.matching}, fetching ${this.isFetchingNewWork}, numCores ${numCores}`);\n return Promise.resolve();\n }\n\n //\n // Oversubscription mitigation.\n // Update when there are less available sandbox slots than numCores.\n const checkNumCores = this.numberOfAvailableSandboxSlots();\n if (numCores > checkNumCores) numCores = checkNumCores;\n if (numCores <= 0) return Promise.resolve();\n\n this.emit('fetchingTask');\n debugging('supervisor') && console.debug('supervisor: fetching task');\n const requestPayload = {\n numCores,\n coreStats: this.getStatisticsCPU(),\n numGPUs: this.defaultMaxGPUs,\n capabilities: this.capabilities,\n paymentAddress: this.paymentAddress,\n jobAddresses: this.options.jobAddresses || [], // force array; when set, only fetches slices for these jobs\n localExec: this.options.localExec,\n workerComputeGroups: this.generateWorkerComputeGroups(),\n minimumWage: dcpConfig.worker.minimumWage || this.options.minimumWage,\n readyJobs: [ /* list of jobs addresses XXXwg */ ],\n previouslyWorkedJobs: this.ringBufferofJobs.buf, //Only discrete jobs\n rejectedJobs: this.rejectedJobs,\n };\n // workers should be part of the public compute group by default\n if (!booley(dcpConfig.worker.leavePublicGroup) && !booley(this.options.leavePublicGroup) && (!requestPayload.localExec))\n requestPayload.workerComputeGroups.push(constants.computeGroups.public);\n debugging('computeGroups') && console.log(`Fetching work for ${requestPayload.workerComputeGroups.length} ComputeGroups: `, requestPayload.workerComputeGroups);\n debugging('supervisor') && console.log(`fetchTask wants ${numCores} slice(s), unallocatedSpace ${this.unallocatedSpace}, queuedSlices ${this.queuedSlices.length}`);\n try {\n debugging('requestTask') && console.debug('fetchTask: requestPayload', requestPayload);\n\n let result = await this.taskDistributorConnection.send('requestTask', requestPayload).catch((error) => {\n debugging('supervisor') && console.error(`Unable to request task from scheduler: ${error}. Will try again on a new connection.`);\n this.taskDistributorConnection.close(error, true);\n throw error; /* caught below */\n });\n let responsePayload = result.payload; \n\n if (!result.success) {\n debugging() && console.log('Task fetch failure; request=', requestPayload);\n debugging() && console.log('Task fetch failure; response=', result.payload);\n throw new DCPError('Unable to fetch task for worker', responsePayload);\n }\n\n const sliceCount = responsePayload.body.task.length || 0;\n\n /**\n * The fetchedTask event fires when the supervisor has finished trying to\n * fetch work from the scheduler (task-manager). The data emitted is the\n * number of new slices to work on in the fetched task.\n *\n * @event Supervisor#fetchedTask\n * @type {number}\n */\n this.emit('fetchedTask', sliceCount);\n\n if (sliceCount < 1) {\n return Promise.resolve();\n }\n\n /**\n * DCP-1698 Send auth msg with tasks to worker, then validate authority of worker to send slice info back to scheduler.\n * payload structure: { owner: this.address, signature: signature, auth: messageLightWeight, body: messageBody };\n * messageLightWeight: { workerId: worker, jobSlices, schedulerId, jobCommissions }\n * messageBody: { newJobs: await getNewJobsForTask(dbScheduler, task, request), task }\n */\n const { body, ...authorizationMessage } = responsePayload;\n const { newJobs, task } = body;\n assert(newJobs); // It should not be possible to have !newJobs -- we throw on !success.\n \n /*\n * Ensure all jobs received from the scheduler are:\n * 1. If we have specified specific jobs the worker may work on, the received jobs are in the specified job list\n * 2. If we are in localExec, at most 1 unique job type was received (since localExec workers are designated for only\n * one job)\n * If the received jobs are not within these parameters, stop the worker since the scheduler cannot be trusted at that point.\n */\n if ((this.options.jobAddresses.length && !Object.keys(newJobs).every((ele) => this.options.jobAddresses.includes(ele)))\n || (this.options.localExec && Object.keys(newJobs).length > 1))\n {\n console.error(\"Worker received slices it shouldn't have. Rejecting the work and stopping.\");\n process.exit(1);\n }\n\n debugging() && console.log(`Supervisor.fetchTask: task: ${task.length}/${numCores}, jobs: ${Object.keys(newJobs).length}, authSlices: ${compressJobMap(authorizationMessage.auth.authSlices, true /* skipFirst*/, this.addressTruncationLength /* digits*/)}`);\n // Delete all jobs in the supervisorCache that are not represented in this task,\n // or in this.queuedSlices, or there is no sandbox assigned to these jobs.\n this.cleanJobCache(newJobs);\n\n for (const jobAddress of Object.keys(newJobs))\n if (!this.cache.cache.job[jobAddress])\n this.cache.store('job', jobAddress, newJobs[jobAddress]);\n\n // Memoize authMessage onto the Slice object, this should\n // follow it for its entire life in the worker.\n const tmpQueuedSlices = task.map(taskElement => new Slice(taskElement, authorizationMessage));\n\n // Make sure old stuff is up front.\n // matchSlicesWithSandboxes dequeues this.queuedSlices as follows:\n // slicesToMatch = this.queuedSlices.slice(0, numCores);\n this.slices.push(...tmpQueuedSlices);\n this.queuedSlices.push(...tmpQueuedSlices);\n \n // Populating the ring buffer based on job's discrete property \n Object.values(newJobs).forEach(job => {\n if(job.requirements.discrete && this.ringBufferofJobs.find(element => element === job.address) === undefined) {\n this.ringBufferofJobs.push(job.address);\n }\n });\n \n } catch (error) {\n this.emit('fetchTaskFailed', error);\n debugging('supervisor') && console.debug(`Supervisor.fetchTask failed!: error: ${error}`);\n }\n }\n\n /**\n * For each slice in this.queuedSlices, match with a sandbox in the following order:\n * 1. Try to find an already assigned sandbox in this.assignedSandboxes for the slice's job.\n * 2. Find a ready sandbox in this.readiedSandboxes that is unassigned.\n * 3. Ready a new sandbox and use that.\n *\n * Take great care in assuring sandboxes and slices are uniquely associated, viz.,\n * a given slice cannot be associated with multiple sandboxes and a given sandbox cannot be associated with multiple slices.\n * The lack of such uniqueness has been the root cause of several difficult bugs.\n *\n * Note: When a sandbox is paired with a slice, execution is pending and sandbox.allocated=true and\n * sandbox.slice=slice and sandbox.jobAddress=slice.jobAddress. This is what 'allocated' means.\n * Immediately upon the exit of sandbox.work, sandbox.allocated=false is set and if an exception\n * wasn't thrown, the paired slice is placed in this.assignedSandboxes.\n * Thus from the pov of supervisor, this.allocatedSandboxes is deterministic and this.workingSandboxes is not.\n * Please try to not use this.workingSandboxes. It is deprecated.\n *\n * The input is numCores, this,queuedSlices, this.assignedSandboxes and this.readiedSandboxes.\n * If there are not enough sandboxes, new readied sandboxes will be created using\n * await this.readySandboxes(...)\n * And it is this await boundary that has caused many bugs.\n * We try not to make assumptions about non-local state across the await boundary.\n *\n * @param {number} numCores - The number of available sandbox slots.\n * @param {boolean} [throwExceptions=true] - Whether to throw exceptions when checking for sanity.\n * @returns {Promise<SandboxSlice[]>} Returns SandboxSlice[], may have length zero.\n */\n async matchSlicesWithSandboxes (numCores, throwExceptions = true) {\n\n const sandboxSlices = [];\n if (this.queuedSlices.length === 0 || this.matching || numCores <= 0) {\n // Interesting and noisy.\n // debugging('supervisor') && console.log(`Supervisor.matchSlicesWithSandboxes: Do not nest work, fetch or matching slices with sandboxes: queuedSlices ${this.queuedSlices.length}, unallocatedSpace ${this.unallocatedSpace}, matching ${this.matching}, fetching ${this.isFetchingNewWork}, numCores ${numCores}`);\n return sandboxSlices;\n }\n\n //\n // Oversubscription mitigation.\n // Update when there are less available sandbox slots than numCores.\n // We cannot use this.unallocatedSpace here because its value is artificially low or zero, because in\n // this.distributedQueuedSlices we use the pseudo-mutex trick: this.acquire(howManySandboxSlotsToReserve)/this.release().\n // Note: Do not use this.numberOfCoresReserved outside of a function locked with this.acquire(howManySandboxSlotsToReserve) .\n const checkNumCores = this.numberOfCoresReserved; // # of locked sandbox slots.\n if (numCores > checkNumCores) numCores = checkNumCores;\n if (numCores <= 0) return sandboxSlices;\n\n // Don't ask for more than we have.\n if (numCores > this.queuedSlices.length)\n numCores = this.queuedSlices.length;\n\n debugging('supervisor') && console.log(`matchSlicesWithSandboxes: numCores ${numCores}, queued slices ${this.queuedSlices.length}: assigned ${this.assignedSandboxes.length}, readied ${this.readiedSandboxes.length}, unallocated ${this.unallocatedSpace}, # of sandboxes: ${this.sandboxes.length}`);\n\n if (debugging('supervisor')) {\n dumpSlicesIfNotUnique(this.queuedSlices, 'Warning: this.queuedSlices slices are not unique -- this is ok when slice is rescheduled.');\n dumpSandboxesIfNotUnique(this.readiedSandboxes, 'Warning: this.readiedSandboxes sandboxes are not unique!');\n dumpSandboxesIfNotUnique(this.assignedSandboxes, 'Warning: this.assignedSandboxes sandboxes are not unique!');\n }\n\n // Three functions to validate slice and sandbox.\n function checkSlice(slice, checkAllocated=true) {\n if (!slice.isUnassigned) throw new DCPError(`Slice must be unassigned: ${slice.identifier}`);\n if (checkAllocated && slice.allocated) throw new DCPError(`Slice must not already be allocated: ${slice.identifier}`);\n }\n function checkSandbox(sandbox, isAssigned) {\n if (sandbox.allocated) throw new DCPError(`Assigned sandbox must not be already allocated: ${sandbox.identifier}`);\n if (isAssigned && !sandbox.isAssigned) throw new DCPError(`Assigned sandbox is not marked as assigned: ${sandbox.identifier}`);\n if (!isAssigned && !sandbox.isReadyForAssign) throw new DCPError(`Readied sandbox is not marked as ready for assign: ${sandbox.identifier}`);\n }\n\n // Sanity checks.\n if (throwExceptions) {\n this.assignedSandboxes.forEach(sandbox => { checkSandbox(sandbox, true /* isAssigned*/); });\n this.readiedSandboxes.forEach(sandbox => { checkSandbox(sandbox, false /* isAssigned*/); });\n this.queuedSlices.forEach(slice => { checkSlice(slice); });\n } else {\n this.assignedSandboxes = this.assignedSandboxes.filter(sandbox => !sandbox.allocated && sandbox.isAssigned);\n this.readiedSandboxes = this.readiedSandboxes.filter(sandbox => !sandbox.allocated && sandbox.isReadyForAssign);\n this.queuedSlices = this.queuedSlices.filter(slice => !slice.allocated && slice.isUnassigned);\n }\n\n const sandboxKind = {\n assigned: 0,\n ready: 1,\n new: 2,\n };\n\n const ceci = this;\n /**\n * Auxiliary function to pair a sandbox with a slice and mark the sandbox as allocated.\n * An allocated sandbox is reserved and will not be released until the slice completes execution on the sandbox.\n *\n * @param {Sandbox} sandbox\n * @param {Slice} slice\n * @param {number} kind\n */\n function pair(sandbox, slice, kind) {\n checkSandbox(sandbox, kind === sandboxKind.assigned);\n checkSlice(slice, kind === sandboxKind.assigned);\n slice.allocated = true;\n sandbox.allocated = true;\n sandbox.jobAddress = slice.jobAddress; // So we can know which jobs to not delete from this.cache .\n sandbox.slice = slice;\n sandboxSlices.push({ sandbox, slice });\n if (Supervisor.sliceTiming) slice['pairingDelta'] = Date.now();\n if (debugging('supervisor')) {\n let fragment = 'New readied';\n if (kind === sandboxKind.assigned) fragment = 'Assigned';\n else if (kind === sandboxKind.ready) fragment = 'Readied';\n console.log(`matchSlicesWithSandboxes.pair: ${fragment} sandbox matched ${ceci.dumpSandboxAndSlice(sandbox, slice)}`);\n }\n }\n\n // These three arrays are used to track/store slices and sandboxes,\n // so that when an exception occurs, the following arrays are restored:\n // this.queuedSlices, this.assignedSandboxes, this.realizedSandboxes.\n let slicesToMatch = [];\n let trackAssignedSandboxes = [];\n let trackReadiedSandboxes = [];\n try\n {\n this.matching = true;\n\n let assignedCounter = 0; // How many assigned sandboxes are being used.\n let readyCounter = 0; // How many sandboxes used from the existing this.readiedSandboxes.\n let newCounter = 0; // How many sandboxes that needed to be newly created.\n\n //\n // The Ideas:\n // 1) We match each slice with a sandbox. First we match with assigned sandboxes in the order\n // that they appear in this.queuedSlices. Then we match in-order with existing this.readiedSandboxes\n // Then we match in-order with new new readied sandboxes created through\n // await this.readySandboxes(newCounter, true /* allocateLocalSandboxes*/);\n // This allows us to try different orderings of execution of slices. E.g. Wes suggested\n // trying to execute slices from different jobs with maximal job diversity -- specifically\n // if there are 3 jobs j1,j2,j3, with slices s11, s12 from j1, s21, s22, s23 from j2 and\n // s31, s32 from j3, then we try to schedule, in order s11, s21, s31, s12, s22, s32, s23.\n //\n // 2) Before matching slices with sandboxes, we allocate available assigned and readied sandboxes\n // and if more are needed then we create and allocate new ones.\n //\n // 3) Finally we match slices with sandboxes and return an array of sandboxSlice pairs.\n //\n // Note: The ordering of sandboxSlices only partially corresponds to the order of this.queuedSlices.\n // It's easy to do. When pairing with assigned sandboxes, any slice in this.queuedSlices which doesn't\n // have an assigned sandbox, will add null to the sandboxSlices array. Then when pairing with readied sandboxes,\n // we fill-in the null entries in the sandboxSlices array.\n //\n /** XXXpfr @todo When it is needed, fix the ordering as described above. */\n\n // Get the slices that are being matched.\n slicesToMatch = this.queuedSlices.slice(0, numCores);\n this.queuedSlices = this.queuedSlices.slice(numCores);\n\n debugging('supervisor') && console.log(`matchSlicesWithSandboxes: slicesToMatch ${this.dumpSlices(slicesToMatch)}`);\n\n // Create object map: jobAddress -> sandboxes with sandboxes.jobAddress === jobAddress .\n const jobSandboxMap = toJobMap(this.assignedSandboxes, sandbox => sandbox);\n \n // Create array to hold slices which do not have assigned sandboxes.\n // These slices will need to be paired with existing and possibly new readied sandboxes.\n // Specifically, the sandboxes from existing this.readiedSandboxes and new sandboxes\n // created through await this.readySandboxes(newCounter, true /* allocateLocalSandboxes*/);\n const slicesThatNeedSandboxes = [];\n\n // Pair assigned sandboxes with slices.\n for (const slice of slicesToMatch) {\n const assigned = jobSandboxMap[slice.jobAddress];\n if (assigned && assigned.length > 0) {\n // Pair.\n const sandbox = assigned.pop();\n pair(sandbox, slice, sandboxKind.assigned);\n this.removeElement(this.assignedSandboxes, sandbox);\n // Track.\n trackAssignedSandboxes.push(sandbox);\n assignedCounter++;\n } else {\n // Don't lose track of these slices.\n slice.allocated = true;\n slicesThatNeedSandboxes.push(slice);\n }\n }\n\n // Pair readied sandboxes with slices.\n readyCounter = Math.min(slicesThatNeedSandboxes.length, this.readiedSandboxes.length);\n newCounter = slicesThatNeedSandboxes.length - readyCounter;\n // Track.\n trackReadiedSandboxes = this.readiedSandboxes.slice(0, readyCounter);\n this.readiedSandboxes = this.readiedSandboxes.slice(readyCounter);\n for (const sandbox of trackReadiedSandboxes) {\n // Pair.\n const slice = slicesThatNeedSandboxes.pop();\n pair(sandbox, slice, sandboxKind.ready);\n }\n \n debugging('supervisor') && console.log(`matchSlicesWithSandboxes: assignedCounter ${assignedCounter}, readyCounter ${readyCounter}, newCounter ${newCounter}, numCores ${numCores}`)\n\n // Validate algorithm consistency.\n if (Supervisor.debugBuild && assignedCounter + readyCounter + newCounter !== numCores) {\n // Structured assert.\n throw new DCPError(`matchSlicesWithSandboxes: Algorithm is corrupt ${assignedCounter} + ${readyCounter} + ${newCounter} !== ${numCores}`);\n }\n\n // Here is an await boundary.\n // Accessing non-local data across an await boundary may result in the unexpected.\n\n // Create new readied sandboxes to associate with slicesThatNeedSandboxes.\n if (newCounter > 0) {\n // When allocateLocalSandboxes is true, this.readySandboxes does not place the new sandboxes\n // on this.readiedSandboxes. Hence the new sandboxes are private and nobody else can see them.\n debugging('supervisor') && console.log(`matchSlicesWithSandboxes: creating ${newCounter} new sandboxes, # of sandboxes ${this.sandboxes.length}`);\n const readied = await this.readySandboxes(newCounter, true /* allocateLocalSandboxes*/);\n // Track.\n trackReadiedSandboxes.push(...readied);\n\n for (const sandbox of readied) {\n assert(slicesThatNeedSandboxes.length > 0);\n // Pair\n const slice = slicesThatNeedSandboxes.pop();\n pair(sandbox, slice, sandboxKind.new);\n }\n \n // Put back any extras. There should not be any unless readySandboxes returned less than asked for.\n if (slicesThatNeedSandboxes.length > 0) {\n slicesThatNeedSandboxes.forEach(slice => {\n slice.allocated = false;\n this.queuedSlices.push(slice);\n });\n }\n }\n\n if ( false || debugging()) {\n console.log(`matchSlicesWithSandboxes: Matches: ${ this.dumpSandboxSlices(sandboxSlices) }`);\n this.dumpSandboxSlicesIfNotUnique(sandboxSlices, 'Warning: sandboxSlices; { sandbox, slice } pairs are not unique!');\n }\n } catch (e) {\n // Clear allocations.\n slicesToMatch.forEach(slice => { slice.allocated = false; });\n trackAssignedSandboxes.forEach(sandbox => { sandbox.allocated = false; sandbox.slice = null; });\n trackReadiedSandboxes.forEach(sandbox => { sandbox.allocated = false; sandbox.slice = null; sandbox.jobAddress = null; });\n \n // Filter out redundancies -- there shouldn't be any...\n slicesToMatch = slicesToMatch.filter(slice => this.queuedSlices.indexOf(slice) === -1);\n trackAssignedSandboxes = trackAssignedSandboxes.filter(sb => this.assignedSandboxes.indexOf(sb) === -1);\n trackReadiedSandboxes = trackReadiedSandboxes.filter(sb => this.readiedSandboxes.indexOf(sb) === -1);\n\n // Sanity checks.\n slicesToMatch.forEach(slice => { checkSlice(slice) });\n trackAssignedSandboxes.forEach(sandbox => { checkSandbox(sandbox, true /* isAssigned*/); });\n trackReadiedSandboxes.forEach(sandbox => { checkSandbox(sandbox, false /* isAssigned*/); });\n\n // Restore arrays.\n this.queuedSlices.push(...slicesToMatch);\n this.assignedSandboxes.push(...trackAssignedSandboxes);\n this.readiedSandboxes.push(...trackReadiedSandboxes);\n \n console.error('Error in matchSlicesWithSandboxes: Attempting to recover slices and sandboxes.', e);\n return [];\n } finally {\n this.matching = false;\n }\n\n debugging('supervisor') && console.log(`matchSlicesWithSandboxes: allocated ${sandboxSlices.length} sandboxes, queuedSlices ${this.queuedSlices.length}, unallocatedSpace ${this.unallocatedSpace}, matching ${this.matching}, fetching ${this.isFetchingNewWork}, # of sandboxes: ${this.sandboxes.length}.`);\n\n return sandboxSlices;\n }\n\n disassociateSandboxAndSlice(sandbox, slice) {\n this.returnSandbox(sandbox);\n sandbox.slice = null;\n this.returnSlice(slice);\n }\n\n /**\n * This method will call this.startSandboxWork(sandbox, slice) for each element { sandbox, slice }\n * of the array returned by this.matchSlicesWithSandboxes(availableSandboxes) until all allocated sandboxes\n * are working. It is possible for a sandbox to interleave with calling distributeQueuedSlices and leave a sandbox\n * that is not working. Moreover, this.queuedSlices may be exhausted before all sandboxes are working.\n * @returns {Promise<void>}\n */\n async distributeQueuedSlices () {\n const numCores = this.unallocatedSpace;\n\n // If there's nothing there, or we're reentering, bail out.\n if (this.queuedSlices.length === 0 || numCores <= 0 || this.matching) {\n // Interesting and noisy.\n // debugging('supervisor') && console.log(`Supervisor.distributeQueuedSlices: Do not nest work, fetch or matching slices with sandboxes: queuedSlices ${this.queuedSlices.length}, matching ${this.matching}, fetching ${this.isFetchingNewWork}, numCores ${numCores}`);\n return Promise.resolve();\n }\n\n //\n // Use the pseudo-mutex to prevent uncontrolled interleaving with fetchTask,\n // matchSlicesWithSandboxes and distributeQueuedSlices\n let sandboxSlices;\n this.acquire(numCores);\n try {\n sandboxSlices = await this.matchSlicesWithSandboxes(numCores);\n } finally {\n this.release();\n }\n\n debugging('supervisor') && console.log(`distributeQueuedSlices: ${sandboxSlices.length} sandboxSlices ${this.dumpSandboxSlices(sandboxSlices)}, matching ${this.matching}, fetching ${this.isFetchingNewWork}`);\n\n for (let sandboxSlice of sandboxSlices) {\n\n const { sandbox, slice } = sandboxSlice;\n try {\n if (sandbox.isReadyForAssign) {\n try {\n let timeoutMs = Math.floor(Math.min(+Supervisor.lastAssignFailTimerMs || 0, 10 * 60 * 1000 /* 10m */));\n await a$sleepMs(timeoutMs);\n await this.assignJobToSandbox(sandbox, slice.jobAddress);\n } catch (e) {\n console.error(`Supervisor.distributeQueuedSlices: Could not assign slice ${slice.identifier} to sandbox ${sandbox.identifier}.`);\n if (Supervisor.debugBuild) console.error(`...exception`, e);\n Supervisor.lastAssignFailTimerMs = Supervisor.lastAssignFailTimerMs ? +Supervisor.lastAssignFailTimerMs * 1.25 : Math.random() * 200;\n this.disassociateSandboxAndSlice(sandbox, slice);\n continue;\n }\n }\n\n if (!Supervisor.lastAssignFailTimerMs)\n Supervisor.lastAssignFailTimerMs = Math.random() * 200;\n this.startSandboxWork(sandbox, slice);\n Supervisor.lastAssignFailTimerMs = false;\n\n } catch (e) {\n // We should never get here.\n console.error(`Supervisor.distributeQueuedSlices: Failed to execute slice ${slice.identifier} in sandbox ${sandbox.identifier}.`);\n if (Supervisor.debugBuild) console.error('...exception', e);\n this.disassociateSandboxAndSlice(sandbox, slice);\n }\n }\n }\n\n /**\n *\n * @param {Sandbox} sandbox\n * @param {opaqueId} jobAddress\n * @returns {Promise<void>}\n */\n async assignJobToSandbox(sandbox, jobAddress) {\n var ceci = this;\n\n try {\n return sandbox.assign(jobAddress); // Returns Promise.\n } catch(error) {\n // return slice to scheduler, log error\n console.error('Supervisor.assignJobToSandbox: Failed to assign job to sandbox.', {\n jobAddress: jobAddress.substr(0,10),\n error,\n });\n\n ceci.returnSandbox(sandbox);\n\n throw error;\n }\n }\n\n /**\n * Handles reassigning or returning a slice that was rejected by a sandbox.\n * \n * The sandbox will be terminated by this.returnSandbox in finalizeSandboxAndSlice. In this case,\n * if the slice does not have a rejected property already, reassign the slice to a new sandbox\n * and add a rejected property to the slice to indicate it has already rejected once, then set slice = null\n * in the return SandboxSlice so that finalizeSandboxAndSlice won't return slice to scheduler.\n * \n * If the slice rejects with a reason, or has a rejected time stamp (ie. has been rejected once already)\n * then return the slice and all slices from the job to the scheduler and\n * terminate all sandboxes with that jobAddress.\n * @param {Sandbox} sandbox \n * @param {Slice} slice\n * @returns {Promise<SandboxSlice>}\n */\n async handleWorkReject(sandbox, slice, rejectReason) {\n if (!this.rejectedJobReasons[slice.jobAddress])\n this.rejectedJobReasons[slice.jobAddress] = [];\n\n this.rejectedJobReasons[slice.jobAddress].push(rejectReason); // memoize reasons\n\n // First time rejecting without a reason. Try assigning slice to a new sandbox.\n if (rejectReason === 'false' && !slice.rejected) {\n // Set rejected.\n slice.rejected = Date.now();\n // Schedule the slice for execution.\n this.scheduleSlice(slice, true /* placeInTheFrontOfTheQueue*/, false /* noDuplicateExecution*/);\n \n // Null out slice so this.returnSlice will not be called in finalizeSandboxAndSlice.\n // But we still want this.returnSandbox to terminate the sandbox.\n slice = null;\n } else { // Slice has a reason OR rejected without a reason already and got stamped.\n \n // Purge all slices and sandboxes associated with slice.jobAddress .\n this.purgeAllWork(slice.jobAddress);\n // Clear jobAddress from this.cache .\n this.cleanJobCache();\n\n // Add to array of rejected jobs.\n let rejectedJob = {\n address: slice.jobAddress,\n reasons: this.rejectedJobReasons[slice.jobAddress],\n }\n this.rejectedJobs.push(rejectedJob);\n\n // Tell everyone all about it, when allowed.\n if (dcpConfig.worker.allowConsoleAccess || Supervisor.debugBuild)\n {\n console.warn('Supervisor.handleWorkReject: The slice ${slice.identifier} was rejected twice; slice will be returned to the scheduler.');\n console.warn('Supervisor.handleWorkReject: All slices with the same jobAddress returned to the scheduler.');\n console.warn('Supervisor.handleWorkReject: All sandboxes with the same jobAddress are terminated.');\n }\n //\n // this.purgeAllWork(jobAddress) terminates all sandboxes with jobAddress,\n // and it also returns to scheduler all slices with jobAddress.\n // Therefore null out slice and sandbox so finalizeSandboxAndSlice doesn't do anything.\n // \n sandbox = null;\n slice = null;\n }\n return { sandbox, slice };\n }\n\n /**\n * Schedule the slice to be executed.\n * If slice is already executing and noDuplicateExecution is true, return the slice with reason.\n * @param {Slice} slice\n * @param {boolean} [placeInTheFrontOfTheQueue=false]\n * @param {boolean} [noDuplicateExecution=true]\n * @param {string} [reason]\n */\n scheduleSlice(slice, placeInTheFrontOfTheQueue = false, noDuplicateExecution = true, reason) {\n // When noDuplicateExecution, if slice is already executing, do nothing.\n let workingSlices = [];\n if (noDuplicateExecution)\n workingSlices = this.allocatedSlices;\n\n if (!workingSlices.indexOf(slice)) {\n // Reset slice state to allow execution.\n slice.status = SLICE_STATUS_UNASSIGNED;\n // Enqueue in the to-be-executed queue.\n if (placeInTheFrontOfTheQueue) this.queuedSlices.unshift(slice);\n else this.queuedSlices.push(slice);\n }\n }\n\n /**\n * Purge all slices and sandboxes with this jobAddress.\n * @param {address} jobAddress\n * @param {boolean} [onlyPurgeQueuedAndAllocated=false]\n */\n purgeAllWork(jobAddress, onlyPurgeQueuedAndAllocated = false) {\n // Purge all slices and sandboxes associated with jobAddress .\n const deadSandboxes = this.sandboxes.filter(sandbox => sandbox.jobAddress === jobAddress);\n\n if (deadSandboxes.length > 0) {\n debugging('supervisor') && console.log(`purgeAllWork(${this.dumpJobAddress(jobAddress)}): sandboxes purged ${deadSandboxes.map(s => s.id)}, # of sandboxes ${this.sandboxes.length}`);\n deadSandboxes.forEach(sandbox => this.returnSandbox(sandbox));\n }\n\n let deadSlices;\n if (onlyPurgeQueuedAndAllocated) {\n deadSlices = this.queuedSlices.filter(slice => slice.jobAddress === jobAddress);\n if (deadSlices.length > 0 || this.allocatedSlices.length > 0)\n debugging('supervisor') && console.log(`purgeAllWork(${this.dumpJobAddress(jobAddress)}): dead queuedSlices ${deadSlices.map(s => s.sliceNumber)}, dead allocatedSlices ${this.allocatedSlices.map(s => s.sliceNumber)}`);\n deadSlices.push(...this.allocatedSlices);\n } else {\n deadSlices = this.slices.filter(slice => slice.jobAddress === jobAddress);\n }\n\n if (deadSlices.length > 0) {\n debugging('supervisor') && console.log(`purgeAllWork(${this.dumpJobAddress(jobAddress)}): slices purged ${deadSlices.map(s => s.sliceNumber)}, # of sandboxes ${this.sandboxes.length}`);\n this.returnSlices(deadSlices);\n this.removeQueuedSlices(deadSlices);\n }\n debugging('supervisor') && console.log(`purgeAllWork(${this.dumpJobAddress(jobAddress)}): Finished: slices ${this.slices.length}, queuedSlices ${this.queuedSlices.length}, assigned ${this.assignedSandboxes.length}, readied ${this.readiedSandboxes.length}, # of sandboxes ${this.sandboxes.length}`);\n }\n\n /**\n * Gives a slice to a sandbox which begins working. Handles collecting\n * the slice result (complete/fail) from the sandbox and submitting the result to the scheduler.\n * It will also return the sandbox to @this.returnSandbox when completed so the sandbox can be re-assigned.\n *\n * @param {Sandbox} sandbox - the sandbox to give the slice\n * @param {Slice} slice - the slice to distribute\n * @returns {Promise<void>} Promise returned from sandbox.run\n */\n async startSandboxWork (sandbox, slice) {\n var startDelayMs, reason = 'unknown';\n\n try {\n slice.markAsWorking();\n } catch (e) {\n // This will occur when the same slice is distributed twice.\n // It is normal because two sandboxes could finish at the same time and be assigned the\n // same slice before the slice is marked as working.\n debugging() && console.debug('startSandboxWork: slice.markAsWorking exception:', e);\n return Promise.resolve();\n }\n\n // sandbox.requiresGPU = slice.requiresGPU;\n // if (sandbox.requiresGPU) {\n // this.GPUsAssigned++;\n // }\n\n if (Supervisor.startSandboxWork_beenCalled)\n startDelayMs = 1000 * (tuning.minSandboxStartDelay + (Math.random() * (tuning.maxSandboxStartDelay - tuning.minSandboxStartDelay)));\n else {\n startDelayMs = 1000 * tuning.minSandboxStartDelay;\n Supervisor.startSandboxWork_beenCalled = true;\n }\n\n try {\n debugging() && console.log(`startSandboxWork: Started ${this.dumpStatefulSandboxAndSlice(sandbox, slice)}, total sandbox count: ${this.sandboxes.length}, matching ${this.matching}, fetching ${this.isFetchingNewWork}`);\n if (Supervisor.sliceTiming) {\n slice['pairingDelta'] = Date.now() - slice['pairingDelta'];\n slice['executionDelta'] = Date.now();\n }\n let result;\n try {\n result = await sandbox.work(slice, startDelayMs);\n } finally {\n sandbox.allocated = false;\n slice.allocated = false;\n }\n if (Supervisor.sliceTiming) {\n slice['executionDelta'] = Date.now() - slice['executionDelta'];\n slice['resultDelta'] = Date.now();\n }\n slice.collectResult(result, true);\n // In watchdog, all sandboxes in working state, have their slice status sent to result submitter.\n // However, this can happen after the sandbox/slice has already sent results\n // to result submitter, in which case, the activeSlices table has already removed the row\n // corresponding to slice and hence is incapable of updating status.\n sandbox.changeWorkingToAssigned();\n this.assignedSandboxes.push(sandbox);\n debugging() && console.log(`startSandboxWork: Finished ${this.dumpStatefulSandboxAndSlice(sandbox, slice)}, total sandbox count: ${this.sandboxes.length}, matching ${this.matching}, fetching ${this.isFetchingNewWork}`);\n } catch(error) {\n let logLevel;\n\n if (error instanceof SandboxError) {\n logLevel = 'warn';\n // The message and stack properties of error objects are not enumerable,\n // so they have to be copied into a plain object this way\n const errorResult = Object.getOwnPropertyNames(error).reduce((o, p) => {\n o[p] = error[p]; return o;\n }, { message: 'Unexpected worker error' });\n slice.collectResult(errorResult, false);\n } else {\n logLevel = 'error';\n // This error was unrelated to the work being done, so just return the slice in the finally block.\n // For extra safety the sandbox is terminated.\n slice.result = null;\n slice.status = SLICE_STATUS_FAILED; /** XXXpfr @todo terminating sandbox? */\n }\n\n let errorString;\n switch (error.errorCode) {\n case 'ENOPROGRESS':\n reason = 'ENOPROGRESS';\n errorString = 'Supervisor.startSandboxWork - No progress error in sandbox.\\n';\n break;\n case 'ESLICETOOSLOW':\n reason = 'ESLICETOOSLOW';\n errorString = 'Supervisor.startSandboxWork - Slice too slow error in sandbox.\\n';\n break;\n case 'EUNCAUGHT':\n reason = 'EUNCAUGHT';\n errorString = `Supervisor.startSandboxWork - Uncaught error in sandbox ${error.message}.\\n`;\n break;\n case 'EFETCH':\n // reason = 'EFETCH'; The status.js processing cannot handle 'EFETCH'\n reason = 'unknown';\n errorString = `Supervisor.startSandboxWork - Could not fetch data: ${error.message}.\\n`;\n break;\n }\n \n // Always display max info under debug builds, otherwise maximal error\n // messages are displayed to the worker, only if both worker and client agree.\n let workerConsole = sandbox.supervisorCache.cache.job[slice.jobAddress].workerConsole;\n const displayMaxInfo = Supervisor.debugBuild || (workerConsole && dcpConfig.worker.allowConsoleAccess);\n\n const errorObject = {\n jobAddress: slice.jobAddress.substr(0,10),\n sliceNumber: slice.sliceNumber,\n sandbox: sandbox.id,\n jobName: sandbox.public ? sandbox.public.name : 'unnamed',\n };\n \n if (error.name === 'EWORKREJECT') {\n error.stack = 'Sandbox was terminated by work.reject()';\n const ss = await this.handleWorkReject(sandbox, slice, error.message);\n sandbox = ss.sandbox; slice = ss.slice;\n }\n\n if (!displayMaxInfo && errorString) {\n console[logLevel](errorString, errorObject);\n } else if (!displayMaxInfo && error.name === 'EWORKREJECT') {\n console[logLevel](`Supervisor.startSandboxWork - Sandbox rejected work: ${error.message}`)\n } else {\n if (displayMaxInfo)\n errorObject.stack += '\\n --------------------\\n' + (error.stack.split('\\n').slice(1).join('\\n'));\n console[logLevel](`Supervisor.startSandboxWork - Sandbox failed: ${error.message}\\n`, errorObject);\n }\n } finally {\n await this.finalizeSandboxAndSlice(sandbox, slice, reason);\n }\n }\n\n /**\n * If slice && slice.result, then call await this.recordResult(slice) and this.returnSandbox(sandbox, slice) will have no effect.\n * If slice && !slice.result, then call this.returnSlice(slice, reason) and then this.returnSandbox(sandbox, slice) which terminates sandbox.\n * If !slice && sandbox, then terminate the sandbox with this.returnSandbox(sandbox, slice) .\n * If !slice && !sandbox, then do nothing.\n * @param {Sandbox} [sandbox]\n * @param {Slice} [slice]\n * @param {string} [reason]\n */\n async finalizeSandboxAndSlice(sandbox, slice, reason) {\n debugging('supervisor') && console.log(`finalizeSandboxAndSlice: sandbox ${sandbox ? sandbox.identifier : 'nade'}, slice ${slice ? slice.identifier : 'nade'}`);\n if (slice) {\n if (slice.result) await this.recordResult(slice);\n else this.returnSlice(slice, reason);\n }\n // It is possible that sandbox is already terminated\n // Because sandbox.allocated=false as soon as sandbox.work(...) completes.\n // But the await at or in finalizeSandboxAndSlice may allow pruneSandboxes to slither in.\n if (sandbox) this.returnSandbox(sandbox, slice, false /* verifySandboxIsNotTerminated*/);\n }\n\n /**\n * Terminates sandboxes and returns slices.\n * Sets the working flag to false, call @this.work to start working again.\n * \n * If forceTerminate is true: Terminates all sandboxes and returns all slices.\n * If forceTerminate is false: Terminates non-allocated sandboxes and returns queued slices.\n *\n * @param {boolean} [forceTerminate = true] - true if you want to stop the sandboxes from completing their current slice.\n * @returns {Promise<void>}\n */\n async stopWork (forceTerminate = true) {\n debugging('supervisor') && console.log('stopWork(${forceTerminate}): terminating sandboxes and returning slices to scheduler.');\n if (forceTerminate) {\n while (this.sandboxes.length) {\n this.returnSandbox(this.sandboxes[0], null, false);\n }\n\n await this.returnSlices(this.slices).then(() => {\n this.queuedSlices.length = 0;\n });\n } else {\n // Only terminate idle sandboxes and return only queued slices\n let idleSandboxes = this.sandboxes.filter(w => !w.allocated);\n for (const sandbox of idleSandboxes) {\n this.returnSandbox(sandbox, null, false /* verifySandboxIsNotTerminated*/);\n }\n\n await this.returnSlices(this.queuedSlices).then(() => {\n this.queuedSlices.length = 0;\n });\n\n await new Promise((resolve, reject) => {\n let sandboxesRemaining = this.allocatedSandboxes.length;\n if (sandboxesRemaining === 0)\n {\n resolve();\n }\n // Resolve and finish work once all sandboxes have finished submitting their results.\n this.on('submitFinished', () => {\n sandboxesRemaining--;\n if (sandboxesRemaining === 0)\n {\n console.log('All sandboxes empty, stopping worker and closing all connections');\n resolve();\n }\n });\n });\n }\n\n if (this.resultSubmitterConnection) {\n this.resultSubmitterConnection.off('close', this.openResultSubmitterConn);\n this.resultSubmitterConnection.close();\n this.resultSubmitterConnection = null;\n }\n\n if (this.taskDistributorConnection) {\n this.taskDistributorConnection.off('close', this.openTaskDistributorConn);\n this.taskDistributorConnection.close();\n this.taskDistributorConnection = null;\n }\n\n if (this.packageManagerConnection) {\n this.packageManagerConnection.off('close', this.openPackageManagerConn);\n this.packageManagerConnection.close();\n this.packageManagerConnection = null;\n }\n\n if (this.eventRouterConnection) {\n this.eventRouterConnection.off('close', this.openEventRouterConn);\n this.eventRouterConnection.close();\n this.eventRouterConnection = null;\n }\n\n this.emit('stop');\n }\n\n /**\n * Takes a slice and returns it to the scheduler to be redistributed.\n * Usually called when an exception is thrown by sandbox.work(slice, startDelayMs) .\n * Or when the supervisor tells it to forcibly stop working.\n *\n * @param {Slice} slice - The slice to return to the scheduler.\n * @param {string} [reason] - Optional reason for the return: 'ENOPROGRESS', 'EUNCAUGHT', 'ESLICETOOSLOW', 'unknown'.\n * @returns {Promise<*>} - Response from the scheduler.\n */\n returnSlice (slice, reason) {\n // When sliceNumber === 0 don't send a status message.\n if (slice.sliceNumber === 0) return Promise.resolve();\n \n debugging() && console.log(`Supervisor.returnSlice: Returning slice ${slice.identifier} with reason ${reason}.`);\n \n const payload = slice.getReturnMessagePayload(this.workerOpaqueId, reason);\n try\n {\n return this.resultSubmitterConnection.send('status', payload) /* resultSubmitterConnection can be null if worker is stopped */\n .then(response => {\n return response;\n }).catch(error => {\n debugging('supervisor') && console.error('Failed to return slice', {\n sliceNumber: slice.sliceNumber,\n jobAddress: slice.jobAddress,\n status: slice.status,\n error,\n }, 'Will try again on a new connection.');\n this.resultSubmitterMessageQueue.push({ operation: 'status', data: payload });\n this.resultSubmitterConnection.close();\n });\n }\n catch (error)\n {\n debugging('supervisor') && console.error(`Failed to return slice ${slice.identifier}, no connection to result submitter:`, error);\n }\n }\n\n /**\n * Bulk-return multiple slices, possibly for assorted jobs.\n * Returns slices to the scheduler to be redistributed.\n * Called in the sandbox terminate handler and purgeAllWork(jobAddress)\n * and stopWork(forceTerminate).\n *\n * @param {Slice[]} slices - The slices to return to the scheduler.\n * @returns {Promise<void>} - Response from the scheduler.\n */\n async returnSlices(slices) {\n if (!slices || !slices.length) return Promise.resolve();\n \n const slicePayload = [];\n slices.forEach(slice => { addToReturnSlicePayload(slicePayload, slice); });\n this.removeSlices(slices);\n\n debugging('supervisor') && console.log(`Supervisor.returnSlices: Returning slices ${this.dumpSlices(slices)}.`);\n\n return this.resultSubmitterConnection.send('status', {\n worker: this.workerOpaqueId,\n slices: slicePayload,\n }).then(response => {\n return response;\n }).catch(error => {\n const errorInfo = slices.map(slice => slice.identifier);\n debugging('supervisor') && console.error('Failed to return slice(s)', { errorInfo, error }, 'Will try again on new connection.');\n this.resultSubmitterMessageQueue.push({ operation: 'status', data: { worker: this.workerOpaqueId, slices: slicePayload } })\n this.resultSubmitterConnection.close();\n // Just in case the caller is expecing a DCP response\n return { success: false, payload: {} };\n });\n }\n\n /**\n * Submits the slice results to the scheduler, either to the\n * work submit or fail endpoints based on the slice status.\n * Then remove the slice from the @this.slices cache.\n *\n * @param {Slice} slice - The slice to submit.\n * @returns {Promise<void>}\n */\n async recordResult (slice) {\n // It is possible for slice.result to be undefined when there are upstream errors.\n if ( !(slice && slice.result))\n throw new Error(`recordResult: slice.result is undefined for slice ${slice.identifier}. This is ok when there are upstream errors.`);\n\n debugging('supervisor') && console.log(`supervisor: recording result for slice ${slice.identifier}.`);\n\n const jobAddress = slice.jobAddress;\n const sliceNumber = slice.sliceNumber;\n const authorizationMessage = slice.getAuthorizationMessage();\n\n /* @see result-submitter::result for full message details */\n const metrics = { GPUTime: 0, CPUTime: 0, CPUDensity: 0, GPUDensity: 0, total: 0 };\n const payloadData = {\n slice: sliceNumber,\n job: jobAddress,\n worker: this.workerOpaqueId,\n paymentAddress: this.paymentAddress,\n metrics,\n authorizationMessage,\n }\n \n const timeReport = slice.timeReport;\n if (timeReport && timeReport.total > 0) {\n metrics.GPUTime = timeReport.webGL;\n metrics.CPUTime = timeReport.CPU;\n metrics.CPUDensity = metrics.CPUTime / timeReport.total;\n metrics.GPUDensity = metrics.GPUTime / timeReport.total;\n metrics.total = timeReport.total;\n metrics.CPUTime = 1 + Math.floor(metrics.CPUTime);\n metrics.GPUTime = 1 + Math.floor(metrics.GPUTime);\n }\n \n this.emit('submittingResult');\n\n if (!slice.isFinished)\n throw new Error('Cannot record result for slice that is not finished');\n\n if (slice.resultStorageType === 'pattern') { /* This is a remote-storage slice. */\n const remoteResult = await this.sendResultToRemote(slice);\n payloadData.result = encodeDataURI(JSON.stringify(remoteResult));\n } else {\n payloadData.result = encodeDataURI(slice.result.result); /* XXXwg - result.result is awful */\n }\n debugging('supervisor') && console.log('Supervisor.recordResult: payloadData.result', payloadData.result.slice(0, 512));\n\n try {\n if (slice.completed) {\n\n /* work function returned a result */\n let resp = await this.resultSubmitterConnection.send(\n 'result',\n payloadData,\n ).catch((error) => {\n debugging('supervisor') && console.error(`Failed to submit result to scheduler for slice ${payloadData.slice} of job ${payloadData.job}:\\n ${error} \\nWill try again on new connection.`);\n this.resultSubmitterMessageQueue.push({ operation: 'result', data: payloadData });\n this.resultSubmitterConnection.close();\n error.handled = true;\n throw error; /* Caught in catch block below */\n });\n \n if (!resp.success)\n throw resp.payload;\n\n if (false) {}\n\n const receipt = {\n accepted: true,\n payment: resp.payload.slicePaymentAmount,\n };\n this.emit('submittedResult', resp.payload);\n this.emit('dccCredit', receipt);\n } else {\n /* slice did not complete for some reason */\n \n // If the slice from a job never completes and the job address exists in the ringBufferofJobs, \n // then we remove it to allow for another slice (from the same job) to be obtained by fetchTask\n this.ringBufferofJobs.buf = this.ringBufferofJobs.filter(element => element !== jobAddress);\n \n await this.returnSlice(slice);\n }\n } catch(error) {\n console.info(`1014: Failed to submit results for slice ${payloadData.slice} of job ${payloadData.job}`, error);\n this.emit('submitSliceFailed', error);\n } finally {\n this.emit('submitFinished');\n // Remove the slice from the slices array.\n this.removeSlice(slice);\n if (Supervisor.sliceTiming) {\n slice['resultDelta'] = Date.now() - slice['resultDelta'];\n console.log(`recordResult(${slice['pairingDelta']}, ${slice['executionDelta']}, ${slice['resultDelta']}): Completed slice ${slice.identifier}.`);\n } else\n debugging('supervisor') && console.log(`recordResult: Completed slice ${slice.identifier}.`);\n }\n }\n\n /**\n * Send a work function's result to a server that speaks our DCP Remote Data Server protocol.\n * The data server dcp-rds is been implemented in https://gitlab.com/Distributed-Compute-Protocol/dcp-rds .\n *\n * @param {Slice} slice - Slice object whose result we are sending.\n * @returns {Promise<object>} - Object of the form { success: true, href: 'http://127.0.0.1:3521/methods/download/jobs/34/result/10' } .\n * @throws When HTTP status not in the 2xx range.\n */\n async sendResultToRemote(slice) {\n const postParams = {\n ...slice.resultStorageParams\n };\n\n const sliceResultUri = makeDataURI('pattern', slice.resultStorageDetails, {\n slice: slice.sliceNumber,\n job: slice.jobAddress,\n });\n\n debugging() && console.log('sendResultToRemote sliceResultUri: ', sliceResultUri);\n\n const url = new DcpURL(sliceResultUri);\n\n // Note: sendResultToRemote was made a member function of class Supervisor to enable access to this.alowedOrigins .\n if (this.allowedOrigins.indexOf(url.origin) === -1 &&\n dcpConfig.worker.allowOrigins.sendResults.indexOf(url.origin) === -1) {\n throw new Error(`Invalid origin for remote result storage: '${url.origin}'`);\n }\n\n postParams.element = slice.sliceNumber;\n postParams.contentType = 'application/json'; // Currently data will be outputed as a JSON object, @todo: Support file upload.\n\n debugging() && console.log('sendResultToRemote: postParams: ', postParams);\n\n let result = slice.result.result;\n if (result) {\n postParams.content = JSON.stringify(result);\n } else {\n postParams.error = JSON.stringify(slice.error);\n }\n\n debugging('supervisor') && console.log('sendResultToRemote: content: ', (result ? postParams.content : postParams.error).slice(0, 512));\n\n //\n // Notes:\n // 1) In recordResults the response from justFetch is JSON serialized and encodeDataURI is called.\n // payloadData.result = await this.sendResultToRemote(slice);\n // payloadData.result = encodeDataURI(JSON.stringify(payloadData.result));\n // 2) We do further processing after the call to sendResultToRemote in recordResult, because\n // if we did it here there would be a perf hit. When the return value is a promise, it gets\n // folded into sendResultToRemote's main promise. If justFetch's promise wasn't a return value then\n // justFetch would be separately added to the micro-task-queue.\n return await justFetch(url, 'JSON', 'POST', false, postParams);\n }\n}\n\n/**\n * Sandbox has had an error which is not from the work function: kill it\n * and try to redo the slice.\n */\nfunction handleSandboxError(supervisor, sandbox, error) {\n const slice = sandbox.slice;\n\n slice.sandboxErrorCount = (slice.sandboxErrorCount || 0) + 1;\n sandbox.slice = null;\n supervisor.returnSandbox(sandbox); /* terminate the sandbox */\n slice.status = SLICE_STATUS_UNASSIGNED; /* ToT */\n console.warn(`Supervisor.handleSandboxError: Sandbox ${sandbox.identifier}...(${sandbox.public.name}/${slice.sandboxErrorCount}) with slice ${slice.identifier} had error.`, error);\n\n if (slice.sandboxErrorCount < dcpConfig.worker.maxSandboxErrorsPerSlice)\n supervisor.queuedSlices.push(slice);\n else {\n slice.error = error;\n supervisor.returnSlice(slice);\n }\n}\n\n/**\n * Add a slice to the slice payload being built. If a sliceList already exists for the\n * job-status-authMessage tuple, then the slice will be added to that, otherwise a new\n * sliceList will be added to the payload.\n *\n * @param {Object[]} slicePayload - Slice payload being built. Will be mutated in place.\n * @param {Slice} slice - The slice.\n * @param {String} status - Status update, eg. progress or scheduled.\n *\n * @returns {Object[]} mutated slicePayload array\n */\nfunction addToSlicePayload(slicePayload, slice, status) {\n // getAuthorizationMessage helps enforces the equivalence\n // !authorizationMessage <==> sliceNumber === 0\n const authorizationMessage = slice.getAuthorizationMessage();\n if (!authorizationMessage) return;\n\n // Try to find a sliceList in the payload which matches the job, status, and auth message\n let sliceList = slicePayload.find(desc => {\n return desc.job === slice.jobAddress\n && desc.status === status\n && desc.authorizationMessage === authorizationMessage;\n });\n\n // If we didn't find a sliceList, start a new one and add it to the payload\n if (!sliceList) {\n sliceList = {\n job: slice.jobAddress,\n sliceNumbers: [],\n status,\n authorizationMessage,\n };\n slicePayload.push(sliceList);\n }\n\n sliceList.sliceNumbers.push(slice.sliceNumber);\n\n return slicePayload;\n}\n\n/**\n * Add a slice to the returnSlice payload being built. If a sliceList already exists for the\n * job-isEstimation-authMessage-reason tuple, then the slice will be added to that, otherwise a new\n * sliceList will be added to the payload.\n *\n * @param {Object[]} slicePayload - Slice payload being built. Will be mutated in place.\n * @param {Slice} slice - The slice.\n * @param {String} [reason] - Optional reason to further characterize status; e.g. 'ENOPROGRESS', 'EUNCAUGHT', 'ESLICETOOSLOW', 'unknown'.\n *\n * @returns {Object[]} mutated slicePayload array\n */\nfunction addToReturnSlicePayload(slicePayload, slice, reason) {\n // getAuthorizationMessage helps enforces the equivalence\n // !authorizationMessage <==> sliceNumber === 0\n const authorizationMessage = slice.getAuthorizationMessage();\n if (!authorizationMessage) return;\n\n if (!reason) reason = slice.error ? 'EUNCAUGHT' : 'unknown';\n\n // Try to find a sliceList in the payload which matches the job, status, and auth message\n let sliceList = slicePayload.find(desc => {\n return desc.job === slice.jobAddress\n && desc.isEstimationSlice === slice.isEstimationSlice\n && desc.authorizationMessage === authorizationMessage\n && desc.reason === reason;\n });\n\n // If we didn't find a sliceList, start a new one and add it to the payload\n if (!sliceList) {\n sliceList = {\n job: slice.jobAddress,\n sliceNumbers: [],\n status: 'return',\n isEstimationSlice: slice.isEstimationSlice,\n authorizationMessage,\n reason,\n };\n slicePayload.push(sliceList);\n }\n\n sliceList.sliceNumbers.push(slice.sliceNumber);\n\n return slicePayload;\n}\n\n/**\n * Return DCPv4-specific connection options, composed of type-specific, URL-specific, \n * and worker-specific options, any/all of which can override the dcpConfig.dcp.connectOptions.\n * The order of precedence is the order of specificity.\n */\nfunction connectionOptions(url, label) {\n return leafMerge(/* ordered from most to least specific */\n dcpConfig.worker.dcp.connectionOptions.default,\n dcpConfig.worker.dcp.connectionOptions[label],\n dcpConfig.worker.dcp.connectionOptions[url.href]);\n}\n\n/** @type {number | boolean} */\nSupervisor.lastAssignFailTimerMs = false;\n/** @type {boolean} */\nSupervisor.startSandboxWork_beenCalled = false;\n/** @type {boolean} */\nSupervisor.debugBuild = ((__webpack_require__(/*! dcp/common/dcp-build */ \"./src/common/dcp-build.js\").build) === 'debug');\n/**\n * When Supervisor.sliceTiming is set to be true, it displays the timings of a every slice\n * slice['pairingDelta'] = timespan of when slice is paired with sandbox until execution starts\n * slice['executionDelta'] = timespan of execution in sandbox\n * slice['resultDelta'] = timespan of when sandbox finishes executing until recordResult completes.\n * @type {boolean}\n */\nSupervisor.sliceTiming = false;\n\nexports.Supervisor = Supervisor;\n\n\n//# sourceURL=webpack://dcp/./src/dcp-client/worker/supervisor.js?");
4515
+ eval("/* provided dependency */ var process = __webpack_require__(/*! ./node_modules/process/browser.js */ \"./node_modules/process/browser.js\");\n/**\n * @file worker/supervisor.js\n *\n * The component that controls each of the sandboxes\n * and distributes work to them. Also communicates with the\n * scheduler to fetch said work.\n *\n * The supervisor readies sandboxes before/while fetching slices.\n * This means sometimes there are extra instantiated WebWorkers\n * that are idle (in this.readiedSandboxes). Readied sandboxes can\n * be used for any slice. After a readied sandbox is given a slice\n * it becomes assigned to slice's job and can only do work\n * for that job.\n *\n * After a sandbox completes its work, the sandbox becomes cached\n * and can be reused if another slice with a matching job is fetched.\n *\n * @author Matthew Palma, mpalma@kingsds.network\n * Ryan Rossiter, ryan@kingsds.network\n * @date May 2019\n */\n\n/* global dcpConfig */\n// @ts-check\n\n\nconst constants = __webpack_require__(/*! dcp/common/scheduler-constants */ \"./src/common/scheduler-constants.js\");\nconst hash = __webpack_require__(/*! dcp/common/hash */ \"./src/common/hash.js\");\nconst wallet = __webpack_require__(/*! dcp/dcp-client/wallet */ \"./src/dcp-client/wallet/index.js\");\nconst protocolV4 = __webpack_require__(/*! dcp/protocol-v4 */ \"./src/protocol-v4/index.js\");\nconst DCP_ENV = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\n\nconst debugging = (__webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope)('worker');\nconst { assert } = __webpack_require__(/*! dcp/common/dcp-assert */ \"./src/common/dcp-assert.js\");\nconst { EventEmitter } = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\");\nconst { Sandbox, SandboxError } = __webpack_require__(/*! ./sandbox */ \"./src/dcp-client/worker/sandbox.js\");\nconst { Slice, SLICE_STATUS_UNASSIGNED, SLICE_STATUS_FAILED } = __webpack_require__(/*! ./slice */ \"./src/dcp-client/worker/slice.js\");\nconst { SupervisorCache } = __webpack_require__(/*! ./supervisor-cache */ \"./src/dcp-client/worker/supervisor-cache.js\");\nconst { DcpURL } = __webpack_require__(/*! dcp/common/dcp-url */ \"./src/common/dcp-url.js\");\nconst { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\nconst { localStorage } = __webpack_require__(/*! dcp/common/dcp-localstorage */ \"./src/common/dcp-localstorage.js\");\nconst { booley, encodeDataURI, makeValueURI, leafMerge, a$sleepMs, justFetch, compressJobMap, toJobMap,\n compressSandboxes, compressSlices, truncateAddress, dumpSandboxesIfNotUnique, dumpSlicesIfNotUnique, \n generateOpaqueId } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\nconst { sliceStatus } = __webpack_require__(/*! dcp/common/scheduler-constants */ \"./src/common/scheduler-constants.js\");\nconst { calculateJoinHash } = __webpack_require__(/*! dcp/dcp-client/compute-groups */ \"./src/dcp-client/compute-groups/index.js\");\nconst RingBuffer = __webpack_require__(/*! dcp/utils/ringBuffer */ \"./src/utils/ringBuffer.js\");\nconst supervisorTuning = dcpConfig.future('worker.tuning');\nconst tuning = {\n watchdogInterval: 7, /**< seconds - time between fetches when ENOTASK(? /wg nov 2019) */\n minSandboxStartDelay: 0.1, /**< seconds - minimum time between WebWorker starts */\n maxSandboxStartDelay: 0.7, /**< seconds - maximum delay time between WebWorker starts */\n ...supervisorTuning\n};\n\n/** Make timers 10x slower when running in niim */\nlet timeDilation = 1;\nif (DCP_ENV.platform === 'nodejs') {\n /** Make timers 10x slower when running in niim */\n timeDilation = (requireNative('module')._cache.niim instanceof requireNative('module').Module) ? 10 : 1;\n}\n\ndcpConfig.future('worker.sandbox', { progressReportInterval: (5 * 60 * 1000) });\nconst sandboxTuning = dcpConfig.worker.sandbox;\n\n/**\n * @typedef {*} address\n * @typedef {*} opaqueId\n */\n\n/**\n * @typedef {object} SandboxSlice\n * @property {Sandbox} sandbox\n * @property {Slice} slice\n */\n\n/**\n * @typedef {object} Signature\n * @property {Uint8Array} r\n * @property {Uint8Array} s\n * @property {Uint8Array} v\n */\n\n/**\n * @typedef {object} SignedAuthorizationMessageObject\n * @property {object} auth\n * @property {Signature} signature\n * @property {module:dcp/wallet.Address} owner\n */\n\n/** @typedef {import('.').Worker} Worker */\n/** @typedef {import('.').SupervisorOptions} SupervisorOptions */\n\nclass Supervisor extends EventEmitter {\n /**\n * @constructor\n * @param {Worker} worker\n * @param {SupervisorOptions} options\n */\n constructor (worker, options={}) {\n super('Supervisor');\n\n /** @type {Worker} */\n this.worker = worker;\n\n /** @type {Sandbox[]} */\n this.sandboxes = [];\n\n /** @type {Sandbox[]} */\n this.readiedSandboxes = [];\n\n /** @type {Sandbox[]} */\n this.assignedSandboxes = [];\n\n /** @type {Slice[]} */\n this.slices = [];\n\n /** @type {Slice[]} */\n this.queuedSlices = [];\n\n /** @type {Slice[]} */\n this.lostSlices = [];\n\n /** @type {boolean} */\n this.matching = false;\n\n /** @type {boolean} */\n this.isFetchingNewWork = false;\n\n /** @type {number} */\n this.numberOfCoresReserved = 0;\n\n /** @type {number} */\n this.addressTruncationLength = 20; // Set to -1 for no truncation.\n\n /** @type {Object[]} */\n this.rejectedJobs = [];\n this.rejectedJobReasons = [];\n\n if (!options) {\n console.error('Supervisor Options', options, new Error().stack);\n options = {};\n }\n\n /** @type {object} */\n this.options = {\n jobAddresses: options.jobAddresses || [/* all jobs unless priorityOnly */],\n ...options,\n };\n\n const { paymentAddress, identity } = options;\n if (paymentAddress) {\n if (paymentAddress instanceof wallet.Keystore) {\n this.paymentAddress = paymentAddress.address;\n } else {\n this.paymentAddress = new wallet.Address(paymentAddress);\n }\n } else {\n this.paymentAddress = null;\n }\n\n this._identityKeystore = identity;\n\n this.extraAllowOrigins = {\n any: [],\n fetchData: [],\n fetchWorkFunctions: [],\n fetchArguments: [],\n sendResults: [],\n };\n \n if (typeof options.allowedOrigins !== 'undefined')\n {\n console.warn('Warning: using deprecated interface options.allowedOrigins; callstack=', new Error().stack.split('\\n').slice(1));\n if (!Array.isArray(options.allowedOrigins))\n {\n for (let kind in this.extraAllowOrigins)\n {\n if (options.allowedOrigins[kind])\n this.extraAllowOrigins[kind].push(...options.allowedOrigins[kind]); \n }\n }\n else\n this.extraAllowOrigins['any'].push(...options.allowedOrigins)\n \n delete options.allowedOrigins;\n }\n /* Assume that an array of dcpConfig.worker.allowOrigins means they can be used for anything */\n if (Array.isArray(dcpConfig.worker.allowOrigins))\n dcpConfig.worker.allowOrigins = { any: dcpConfig.worker.allowOrigins };\n \n /**\n * Maximum sandboxes allowed to work at a given time.\n * @type {number}\n */\n this.maxWorkingSandboxes = options.maxWorkingSandboxes || 1;\n\n /** @type {number} */\n this.defaultMaxGPUs = 1;\n // this.GPUsAssigned = 0;\n \n // Object.defineProperty(this, 'GPUsAssigned', {\n // get: () => this.allocatedSandboxes.filter(sb => !!sb.requiresGPU).length,\n // enumerable: true,\n // configurable: false,\n // });\n\n /**\n * TODO: Remove this when the supervisor sends all of the sandbox\n * capabilities to the scheduler when fetching work.\n * @type {object}\n */\n this.capabilities = null;\n\n /** @type {number} */\n this.lastProgressReport = 0;\n\n /** \n * An N-slot ring buffer of job addresses. Stores all jobs that have had no more than 1 slice run in the ring buffer.\n * Required for the implementation of discrete jobs \n * @type {RingBuffer} \n */\n this.ringBufferofJobs = new RingBuffer(200); // N = 200 should be more than enough.\n \n // @hack - dcp-env.isBrowserPlatform is not set unless the platform is _explicitly_ set,\n // using the default detected platform doesn't set it.\n // Fixing that causes an error in the wallet module's startup on web platform, which I\n // probably can't fix in a reasonable time this morning.\n // ~ER2020-02-20\n\n if (!options.maxWorkingSandboxes\n && DCP_ENV.browserPlatformList.includes(DCP_ENV.platform)\n && navigator.hardwareConcurrency > 1) {\n this.maxWorkingSandboxes = navigator.hardwareConcurrency - 1;\n if (typeof navigator.userAgent === 'string') {\n if (/(Android).*(Chrome|Chromium)/.exec(navigator.userAgent)) {\n this.maxWorkingSandboxes = 1;\n console.log('Doing work with Chromimum browsers on Android is currently limited to one sandbox');\n }\n }\n }\n\n /** @type {SupervisorCache} */\n this.cache = new SupervisorCache(this);\n /** @type {object} */\n this._connections = {}; /* active DCPv4 connections */\n // Call the watchdog every 7 seconds.\n this.watchdogInterval = setInterval(() => this.watchdog(), tuning.watchdogInterval * 1000);\n if (DCP_ENV.platform === 'nodejs' && this.options.localExec) /* interval helps keep normal worker alive forever, which we don't want in localexec */\n this.watchdogInterval.unref();\n\n const ceci = this;\n\n // Initialize to null so these properties are recognized for the Supervisor class\n this.taskDistributorConnection = null;\n this.eventRouterConnection = null;\n this.resultSubmitterConnection = null;\n this.packageManagerConnection = null;\n this.openTaskDistributorConn = function openTaskDistributorConn()\n {\n let config = dcpConfig.scheduler.services.taskDistributor;\n ceci.taskDistributorConnection = new protocolV4.Connection(config, ceci.identityKeystore, connectionOptions(config.location, 'taskDistributor'));\n ceci.taskDistributorConnection.on('close', ceci.openTaskDistributorConn);\n }\n\n this.openEventRouterConn = function openEventRouterConn()\n {\n let config = dcpConfig.scheduler.services.eventRouter;\n ceci.eventRouterConnection = new protocolV4.Connection(config, ceci.identityKeystore, connectionOptions(config.location, 'eventRouter'));\n ceci.eventRouterConnection.on('close', ceci.openEventRouterConn);\n if (ceci.eventRouterMessageQueue.length)\n ceci.resendRejectedMessages(ceci.eventRouterConnection, ceci.eventRouterMessageQueue);\n }\n this.eventRouterMessageQueue = [];\n \n this.openResultSubmitterConn = function openResultSubmitterConn()\n {\n let config = dcpConfig.scheduler.services.resultSubmitter;\n ceci.resultSubmitterConnection = new protocolV4.Connection(config, ceci.identityKeystore, connectionOptions(config.location, 'resultSubmitter'));\n ceci.resultSubmitterConnection.on('close', ceci.openResultSubmitterConn);\n if (ceci.resultSubmitterMessageQueue.length)\n ceci.resendRejectedMessages(ceci.resultSubmitterConnection, ceci.resultSubmitterMessageQueue);\n }\n this.resultSubmitterMessageQueue = [];\n\n this.openPackageManagerConn = function openPackageManagerConn()\n {\n let config = dcpConfig.packageManager;\n ceci.packageManagerConnection = new protocolV4.Connection(config, ceci.identityKeystore, connectionOptions(config.location, 'packageManager'));\n ceci.packageManagerConnection.on('close', ceci.openPackageManagerConn);\n if (ceci.packageManagerMessageQueue.length)\n ceci.resendRejectedMessages(ceci.packageManagerConnection, ceci.packageManagerMessageQueue);\n }\n this.packageManagerMessageQueue = [];\n }\n\n /**\n * Return worker opaqueId.\n * @type {opaqueId}\n */\n get workerOpaqueId() {\n if (!this._workerOpaqueId)\n this._workerOpaqueId = localStorage.getItem('workerOpaqueId');\n\n if (!this._workerOpaqueId || this._workerOpaqueId.length !== constants.workerIdLength) {\n this._workerOpaqueId = generateOpaqueId();\n localStorage.setItem('workerOpaqueId', this._workerOpaqueId);\n }\n\n return this._workerOpaqueId;\n }\n\n /**\n * This getter is the absolute source-of-truth for what the\n * identity keystore is for this instance of the Supervisor.\n */\n get identityKeystore() {\n assert(this.defaultIdentityKeystore);\n\n return this._identityKeystore || this.defaultIdentityKeystore;\n }\n \n \n /** \n * Factory function which generates a list of origins which are safe to communicate \n * with for this purpose. Currently-valid purposes (more will be added):\n * - any\n * - fetchData\n * - fetchWork\n * - fetchWorkArguments\n * - sendResults\n */\n makeSafeOriginList(purpose)\n {\n var list = [];\n\n assert(Array.isArray(this.extraAllowOrigins[purpose]));\n \n if (this.extraAllowOrigins[purpose])\n list = list.concat(this.extraAllowOrigins[purpose]);\n if (dcpConfig.worker.allowOrigins[purpose])\n list = list.concat(dcpConfig.worker.allowOrigins[purpose])\n\n // In localExec, do not allow work function or arguments to come from the 'any' origins\n if (purpose !== 'any' && (!this.options.localExec || (this.options.localExec && purpose === 'sendResults')))\n {\n if (this.extraAllowOrigins.any)\n list = list.concat(this.extraAllowOrigins.any);\n if (dcpConfig.worker.allowOrigins.any)\n list = list.concat(dcpConfig.worker.allowOrigins.any);\n }\n return list;\n }\n\n /**\n * Open all connections. Used when supervisor is instantiated or stopped/started\n * to initially open connections.\n */\n instantiateAllConnections() {\n if (!this.taskDistributorConnection)\n this.openTaskDistributorConn();\n \n if (!this.eventRouterConnection)\n this.openEventRouterConn();\n \n if (!this.resultSubmitterConnection)\n this.openResultSubmitterConn();\n\n if (!this.packageManagerConnection)\n this.openPackageManagerConn();\n }\n \n /**\n * Asynchronously send a result to the result submitter that was previously rejected.\n * Different from resendRejectedMessages below in the sense that the function only resolves\n * once we've delivered the result, or gone past our max number of attempts.\n * @param {object} result \n * @returns the response payload from the result operation\n */\n async resendResult(result) {\n var protocolError = false;\n if (!result.sendRetries)\n result.sendRetries = 1;\n else\n result.sendRetries++;\n \n if (result.sendRetries > dcpConfig.worker.maxResultSubmissionRetries)\n throw new DCPError(`Could not submit result after ${dcpConfig.worker.maxResultSubmissionRetries} attempts. Aborting.`) \n \n debugging() && console.debug(`supervisor - failed to submit result ${result.sendRetries} time(s), trying again `)\n let res = await this.resultSubmitterConnection.send('result', result).catch(async (e) => {\n debugging('supervisor') && console.error(`Failed to submit result to scheduler for slice ${result.slice} of job ${result.job}:\\n ${e} \\nWill try again on new connection.`);\n this.resultSubmitterConnection.close();\n await a$sleepMs(10); /* let connection recycle */\n protocolError = true;\n });\n if ((!res.success && res.payload.code === 'DCPS-01002') || protocolError)\n return this.resendResult(result)\n else\n return res;\n }\n \n /**\n * Try sending messages that were rejected on an old instance of the given connection.\n * These are messages that a) were rejected due to a protocol error and b) don't care when exactly\n * they're sent in the grand scheme of things.\n */\n resendRejectedMessages(connection, messageQueue) {\n if (connection.resendingMessages) /* if the passed connection is already in the loop, exit */\n return;\n \n var message = messageQueue.shift();\n\n do {\n \n connection.resendingMessages = true;\n var quitLoop = false;\n \n connection.send(message.operation, message.data)\n .catch((e) =>\n {\n /* Protocol Error; Close connection (this will trigger the opening of a new connection that will try sending again) */\n debugging('supervisor') && console.error(`Failed to send message ${message.operation} to scheduler: ${e}. Will try again on a new \n connection.`);\n messageQueue.unshift(message);\n connection.close();\n quitLoop = true;\n });\n \n message = messageQueue.shift();\n \n } while (message && !quitLoop)\n\n connection.resendingMessages = false;\n }\n\n /** Set the default identity keystore -- needs to happen before anything that talks\n * to the scheduler for work gets called. This is a wart and should be removed by\n * refactoring.\n *\n * The default identity keystore will be used if the Supervisor was not provided\n * with an alternate. This keystore will be located via the Wallet API, and \n * if not found, a randomized default identity will be generated. \n *\n * @param {object} ks An instance of wallet::Keystore -- if undefined, we pick the best default we can.\n * @returns {Promise<void>}\n */\n async setDefaultIdentityKeystore(ks) {\n try {\n if (ks) {\n this.defaultIdentityKeystore = ks;\n return;\n }\n\n if (this.defaultIdentityKeystore)\n return;\n\n try {\n this.defaultIdentityKeystore = await wallet.getId();\n } catch(e) {\n debugging('supervisor') && console.debug('Error generating default identity, try to do it another way.');\n this.defaultIdentityKeystore = await new wallet.IdKeystore(null, '');\n }\n } finally {\n if (this.defaultIdentityKeystore)\n debugging('supervisor') && console.debug('Set default identity =', this.defaultIdentityKeystore.address);\n else\n debugging('supervisor') && console.debug('Failed to set default identity, worker cannot work.');\n }\n }\n\n //\n // What follows is a bunch of utility properties and functions for creating filtered views\n // of the slices and sandboxes array.\n //\n /** XXXpfr @todo Write sort w/o using promises so we can get rid of async on all the compress functions. */\n\n /**\n * @deprecated -- Please do not use this.workingSandboxes; use this.allocatedSandboxes instead.\n * Sandboxes that are in WORKING state.\n *\n * Warning: Do not rely on this information being 100% accurate -- it may change in the next instant.\n * @type {Sandbox[]}\n */\n get workingSandboxes() {\n return this.sandboxes.filter(sandbox => sandbox.isWorking);\n }\n\n /**\n * Use instead of this.workingSandboxes.\n *\n * When a sandbox is paired with a slice, execution is pending and sandbox.allocated=true and\n * sandbox.slice=slice and sandbox.jobAddress=slice.jobAddress. This is what 'allocated' means.\n * Immediately upon the exit of sandbox.work, sandbox.allocated=false is set and if an exception\n * wasn't thrown the sandbox is placed in this.assignedSandboxes.\n * Thus from the pov of supervisor, this.allocatedSandboxes is deterministic and this.workingSandboxes is not.\n * Please try to not use this.workingSandboxes. It is deprecated.\n *\n * Warning: Do not rely on this information being 100% accurate -- it may change in the next instant.\n * @type {Sandbox[]}\n */\n get allocatedSandboxes() {\n return this.sandboxes.filter(sandbox => sandbox.allocated);\n }\n\n /**\n * Slices that are allocated.\n * Warning: Do not rely on this information being 100% accurate -- it may change in the next instant.\n * @type {Slice[]}\n */\n get allocatedSlices() {\n return this.slices.filter(slice => slice.allocated);\n }\n\n /**\n * This property is used as the target number of sandboxes to be associated with slices and start working.\n *\n * It is used in this.watchdog as to prevent a call to this.work when unallocatedSpace <= 0.\n * It is also used in this.distributeQueuedSlices where it is passed as an argument to this.matchSlicesWithSandboxes to indicate how many sandboxes\n * to associate with slices and start working.\n *\n * Warning: Do not rely on this information being 100% accurate -- it may change in the next instant.\n * @type {number}\n */\n get unallocatedSpace() {\n return this.maxWorkingSandboxes - this.allocatedSandboxes.length - this.numberOfCoresReserved;\n }\n \n /**\n * Call acquire(numberOfCoresToReserve) to reserve numberOfCoresToReserve unallocated sandboxes as measured by unallocatedSpace.\n * Call release() to undo the previous acquire.\n * This pseudo-mutex technique helps prevent races in scheduling slices in Supervisor.\n * @param {number} numberOfCoresToReserve\n */\n acquire(numberOfCoresToReserve) { \n this.numberOfCoresReserved = numberOfCoresToReserve; \n }\n release() { \n this.numberOfCoresReserved = 0; \n }\n\n /**\n * Remove from this.slices.\n * @param {Slice} slice\n */\n removeSlice(slice) {\n this.removeElement(this.slices, slice);\n if (Supervisor.debugBuild) {\n if (this.queuedSlices.indexOf(slice) !== -1)\n throw new Error(`removeSlice: slice ${slice.identifier} is in queuedSlices; inconsistent state.`);\n if (this.lostSlices.length > 0) {\n console.warn(`removeSlice: slice ${slice.identifier}, found lostSlices ${this.lostSlices.map(s => s.identifier)}`);\n if (this.lostSlices.indexOf(slice) !== -1)\n throw new Error(`removeSlice: slice ${slice.identifier} is in lostSlices; inconsistent state.`);\n }\n }\n }\n\n /**\n * Remove from this.slices.\n * @param {Slice[]} slices\n */\n removeSlices(slices) {\n this.slices = this.slices.filter(slice => slices.indexOf(slice) === -1);\n }\n\n /**\n * Remove from this.queuedSlices.\n * @param {Slice[]} slices\n */\n removeQueuedSlices(slices) {\n this.queuedSlices = this.queuedSlices.filter(slice => slices.indexOf(slice) === -1);\n }\n\n /**\n * Remove from this.sandboxes, this.assignedSandboxes and this.readiedSandboxes.\n * @param {Sandbox} sandbox\n */\n removeSandbox(sandbox) {\n debugging('scheduler') && console.log(`removeSandbox ${sandbox.identifier}`);\n this.removeElement(this.sandboxes, sandbox);\n this.removeElement(this.assignedSandboxes, sandbox);\n\n // XXXpfr: April 13, 2022\n // I'm trying to understand and control when sandboxes get removed.\n // A sandbox in this.readiedSandboxes should never have returnSandbox/removeSandbox called on it except in stopWork.\n // Because of races and random worker crashes, it is hard to get this right, but I want to try.\n // If I don't fix this is the next 30 days or I forget, please delete this exception.\n if (false)\n {}\n\n this.removeElement(this.readiedSandboxes, sandbox);\n }\n\n /**\n * Remove from this.sandboxes and this.assignedSandboxes .\n * @param {Sandbox[]} sandboxes\n */\n async removeSandboxes(sandboxes) {\n debugging('scheduler') && console.log(`removeSandboxes: Remove ${sandboxes.length} sandboxes ${this.dumpSandboxes(sandboxes)}`);\n this.sandboxes = this.sandboxes.filter(sandbox => sandboxes.indexOf(sandbox) === -1);\n this.assignedSandboxes = this.assignedSandboxes.filter(sandbox => sandboxes.indexOf(sandbox) === -1);\n\n if (Supervisor.debugBuild) {\n const readied = this.readiedSandboxes.filter(sandbox => sandboxes.indexOf(sandbox) !== -1);\n if (readied.length > 0)\n throw new Error(`removeSandboxes: sandboxes ${readied.map(s => s.identifier)} are in readiedSandboxes; inconsistent state.`);\n }\n }\n\n /**\n * Remove element from theArray.\n * @param {Array<*>} theArray\n * @param {object|number} element\n * @param {boolean} [assertExists = true]\n */\n removeElement(theArray, element, assertExists = false) {\n let index = theArray.indexOf(element);\n assert(index !== -1 || !assertExists);\n if (index !== -1) theArray.splice(index, 1);\n }\n\n /**\n * Log sliceArray.\n * @param {Slice[]} sliceArray\n * @param {string} [header]\n * @returns {string}\n */\n dumpSlices(sliceArray, header) {\n if (header) console.log(`\\n${header}`);\n return compressSlices(sliceArray, this.addressTruncationLength);\n }\n\n /**\n * Log sandboxArray.\n * @param {Sandbox[]} sandboxArray\n * @param {string} [header]\n * @returns {string}\n */\n dumpSandboxes(sandboxArray, header) {\n if (header) console.log(`\\n${header}`);\n return compressSandboxes(sandboxArray, this.addressTruncationLength);\n }\n\n /**\n * If the elements of sandboxSliceArray are not unique, log the duplicates and dump the array.\n * @param {SandboxSlice[]} sandboxSliceArray\n * @param {string} header\n */\n dumpSandboxSlicesIfNotUnique(sandboxSliceArray, header) {\n if (!this.isUniqueSandboxSlices(sandboxSliceArray, header))\n console.log(this.dumpSandboxSlices(sandboxSliceArray));\n }\n\n /**\n * Log { sandbox, slice }.\n * @param {Sandbox} sandbox\n * @param {Slice} slice\n * @returns {string}\n */\n dumpSandboxAndSlice(sandbox, slice) {\n return `${sandbox.id}~${slice.sliceNumber}.${this.dumpJobAddress(slice.jobAddress)}`;\n }\n\n /**\n * Log { sandbox, slice } with state/status.\n * @param {Sandbox} sandbox\n * @param {Slice} slice\n * @returns {string}\n */\n dumpStatefulSandboxAndSlice(sandbox, slice) {\n return `${sandbox.id}.${sandbox.state}~${slice.sliceNumber}.${this.dumpJobAddress(slice.jobAddress)}.${slice.status}`;\n }\n\n /**\n * Truncates jobAddress.toString() to this.addressTruncationLength digits.\n * @param {address} jobAddress\n * @returns {string}\n */\n dumpJobAddress(jobAddress) {\n return truncateAddress(jobAddress, this.addressTruncationLength /* digits*/);\n }\n\n /**\n * Dump sandboxSliceArray.\n * @param {SandboxSlice[]} sandboxSliceArray - input array of { sandbox, slice }\n * @param {string} [header] - optional header\n * @param {boolean} [stateFul] - when true, also includes slice.status and sandbox.state.\n * @returns {string}\n */\n dumpSandboxSlices(sandboxSliceArray, header, stateFul=false) {\n if (header) console.log(`\\n${header}`);\n const jobMap = {};\n sandboxSliceArray.forEach(ss => {\n const sss = stateFul ? `${ss.sandbox.id}.${ss.sandbox.state}~${ss.slice.sliceNumber}.${ss.slice.status}` : `${ss.sandbox.id}~${ss.slice.sliceNumber}`;\n if (!jobMap[ss.slice.jobAddress]) jobMap[ss.slice.jobAddress] = sss;\n else jobMap[ss.slice.jobAddress] += `,${sss}`;\n });\n let output = '';\n for (const [jobAddress, sss] of Object.entries(jobMap))\n output += `${this.dumpJobAddress(jobAddress)}:[${sss}]:`;\n return output;\n }\n\n /**\n * Check sandboxSliceArray for duplicates.\n * @param {SandboxSlice[]} sandboxSliceArray\n * @param {string} [header]\n * @param {function} [log]\n * @returns {boolean}\n */\n isUniqueSandboxSlices(sandboxSliceArray, header, log) {\n const result = [], slices = [], sandboxes = [];\n let once = true;\n sandboxSliceArray.forEach(x => {\n const sliceIndex = slices.indexOf(x.slice);\n const sandboxIndex = sandboxes.indexOf(x.sandbox);\n\n if (sandboxIndex >= 0) {\n if (once && header) console.log(`\\n${header}`); once = false;\n log ? log(x.sandbox) : console.log(`\\tWarning: Found duplicate sandbox ${x.sandbox.identifier}.`);\n } else sandboxes.push(x.sandbox);\n\n if (sliceIndex >= 0) {\n if (once && header) console.log(`\\n${header}`); once = false;\n log ? log(x.slice) : console.log(`\\tWarning: Found duplicate slice ${x.slice.identifier}.`);\n } else {\n slices.push(x.slice);\n if (sandboxIndex < 0) result.push(x);\n }\n });\n return sandboxSliceArray.length === result.length;\n }\n\n /**\n * Attempts to create and start a given number of sandboxes.\n * The sandboxes that are created can then be assigned for a\n * specific job at a later time. All created sandboxes\n * get put into the @this.readiedSandboxes array when allocateLocalSandboxes is false.\n *\n * @param {number} numSandboxes - the number of sandboxes to create\n * @param {boolean} [allocateLocalSandboxes=false] - when true, do not place in this.readiedSandboxes\n * @returns {Promise<Sandbox[]>} - resolves with array of created sandboxes, rejects otherwise\n * @throws when given a numSandboxes is not a number or if numSandboxes is Infinity\n */\n async readySandboxes (numSandboxes, allocateLocalSandboxes = false) {\n debugging('supervisor') && console.debug(`readySandboxes: Readying ${numSandboxes} sandboxes, total sandboxes ${this.sandboxes.length}, matching ${this.matching}, fetching ${this.isFetchingNewWork}`);\n \n if (typeof numSandboxes !== 'number' || Number.isNaN(numSandboxes) || numSandboxes === Infinity) {\n throw new Error(`${numSandboxes} is not a number of sandboxes that can be readied.`);\n }\n if (numSandboxes <= 0) {\n return [];\n }\n\n const sandboxStartPromises = [];\n const sandboxes = [];\n const errors = [];\n for (let i = 0; i < numSandboxes; i++) {\n const sandbox = new Sandbox(this, {\n ...this.options.sandboxOptions,\n });\n sandbox.addListener('ready', () => this.emit('sandboxReady', sandbox));\n sandbox.addListener('start', () => {\n this.emit('sandboxStart', sandbox);\n\n // When sliceNumber == 0, result-submitter status skips the slice,\n // so don't send it in the first place.\n // The 'start' event is fired when a worker starts up, hence there's no way\n // to determine whether sandbox has a valid slice without checking.\n if (sandbox.slice) {\n const jobAddress = sandbox.jobAddress;\n const sliceNumber = sandbox.slice.sliceNumber;\n // !authorizationMessage <==> sliceNumber === 0.\n const authorizationMessage = sandbox.slice.getAuthorizationMessage();\n\n if (authorizationMessage) {\n let statusPayload = {\n worker: this.workerOpaqueId,\n slices: [{\n job: jobAddress,\n sliceNumber: sliceNumber,\n status: 'begin',\n authorizationMessage,\n }],\n }\n \n try /* resultSubmitterConnection can be null if worker is stopped */\n {\n this.resultSubmitterConnection.send('status', statusPayload).catch((error) => {\n debugging('supervisor') && console.error(`Error sending 'status' for slice ${sliceNumber} of job ${jobAddress}:\\n ${error}\\nWill try again on a new connection`);\n this.resultSubmitterMessageQueue.push({ operation: 'status', data: statusPayload });\n this.resultSubmitterConnection.close();\n });\n } catch (error)\n {\n debugging('supervisor') && console.error(`Failed to send 'status' for slice ${sliceNumber} of job ${jobAddress}:, no connection to result submitter:`, error);\n }\n }\n }\n });\n sandbox.addListener('workEmit', ({ eventName, payload }) => {\n // Need to check if the sandbox hasn't been assigned a slice yet.\n if (!sandbox.slice) {\n if (Supervisor.debugBuild) {\n console.error(\n `Sandbox not assigned a slice before sending workEmit message to scheduler. 'workEmit' event originates from \"${eventName}\" event`, \n payload,\n );\n }\n }\n else\n {\n const jobAddress = sandbox.slice.jobAddress;\n const sliceNumber = sandbox.slice.sliceNumber;\n // sliceNumber can be zero if it came from a problem with loading modules.\n assert(jobAddress && (sliceNumber || sliceNumber === 0));\n // Send a work emit message from the sandbox to the event router\n // !authorizationMessage <==> sliceNumber === 0.\n let authorizationMessage;\n try {\n // Sometimes a sliceNumber===0 workEmit comes in before the client bundle is properly loaded.\n // Also happens with minor dcp-client version mismatches.\n authorizationMessage = sandbox.slice.getAuthorizationMessage();\n } catch(e) {\n authorizationMessage = null;\n }\n\n if (!authorizationMessage)\n {\n console.warn(`workEmit: missing authorization message for job ${jobAddress}, slice: ${sliceNumber}`);\n return Promise.resolve();\n }\n \n let workEmitPayload = {\n eventName,\n payload,\n job: jobAddress,\n slice: sliceNumber,\n worker: this.workerOpaqueId,\n authorizationMessage,\n }\n \n const workEmitPromise = this.eventRouterConnection.send('workEmit', workEmitPayload).catch(error => {\n debugging('supervisor') && console.warn(`workEmit: unable to send ${eventName} for slice ${sliceNumber} of job ${jobAddress}: ${error.message}.\\nTrying again on a new connection.`);\n this.eventRouterMessageQueue.push({ operation: 'workEmit', data: workEmitPayload })\n this.eventRouterConnection.close();\n if (Supervisor.debugBuild)\n console.error('workEmit error:', error);\n });\n\n if (Supervisor.debugBuild) {\n workEmitPromise.then(result => {\n if (!result || !result.success)\n console.warn('workEmit: event router did not accept event', result);\n });\n }\n }\n });\n\n // When any sbx completes, \n sandbox.addListener('complete', () => {\n this.watchdog();\n });\n\n sandbox.on('sandboxError', (error) => handleSandboxError(this, sandbox, error));\n \n sandbox.on('rejectedWorkMetrics', (data) =>{\n function updateRejectedMetrics(report) {\n ['total', 'CPU', 'webGL'].forEach((key) => {\n if (report[key]) sandbox.slice.rejectedTimeReport[key] += report[key];\n })\n }\n \n // If the slice already has rejected metrics, add this data to it. If not, assign this data to slices rejected metrics property\n if (sandbox.slice) {\n (sandbox.slice.rejectedTimeReport) ? updateRejectedMetrics(data.timeReport) : sandbox.slice.rejectedTimeReport = data.timeReport;\n }\n })\n \n // If the sandbox terminated and we are not shutting down, then should return all work which is currently\n // not being computed if all sandboxes are dead and the attempt to create a new one fails.\n sandbox.on('terminated',async () => {\n if (this.sandboxes.length > 0) {\n let terminatedSandboxes = this.sandboxes.filter(sbx => sbx.isTerminated);\n if (terminatedSandboxes.length === this.sandboxes.length) {\n debugging('supervisor') && console.debug(`readySandboxes: Create 1 sandbox in the sandbox-terminated-handler, total sandboxes ${this.sandboxes.length}, matching ${this.matching}, fetching ${this.isFetchingNewWork}`);\n await this.readySandboxes(1);\n \n // If we cannot create a new sandbox, that probably means we're on a screensaver worker\n // and the screensaver is down. So return the slices to the scheduler.\n if (this.sandboxes.length !== terminatedSandboxes.length + 1) {\n this.returnSlices(this.queuedSlices).then(() => {\n this.queuedSlices.length = 0;\n });\n }\n }\n }\n })\n\n const delayMs =\n 1000 *\n (tuning.minSandboxStartDelay +\n Math.random() *\n (tuning.maxSandboxStartDelay - tuning.minSandboxStartDelay));\n \n sandboxStartPromises.push(\n sandbox\n .start(delayMs)\n .then(() => {\n if (!allocateLocalSandboxes) this.readiedSandboxes.push(sandbox);\n this.sandboxes.push(sandbox);\n sandboxes.push(sandbox);\n }).catch((err) => {\n errors.push(err);\n this.returnSandbox(sandbox);\n if (err.code === 'ENOWORKER') {\n throw new DCPError(\"Cannot use localExec without dcp-worker installed. Use the command 'npm install dcp-worker' to install the neccessary modules.\", 'ENOWORKER');\n }\n }));\n }\n \n await Promise.all(sandboxStartPromises);\n\n if (errors.length) {\n console.warn(`Failed to ready ${errors.length} of ${numSandboxes} sandboxes.`, errors);\n throw new Error('Failed to ready sandboxes.');\n }\n\n debugging('supervisor') && console.log(`readySandboxes: Readied ${sandboxes.length} sandboxes ${JSON.stringify(sandboxes.map(sandbox => sandbox.id))}`);\n \n return sandboxes;\n }\n\n /**\n * Accepts a sandbox after it has finished working or encounters an error.\n * If the sandbox was terminated or if \"!slice || slice.failed\" then\n * the sandbox will be removed from the sandboxes array and terminated if necessary.\n * Otherwise it will try to distribute a slice to the sandbox immediately.\n *\n * @param {Sandbox} sandbox - the sandbox to return\n * @param {Slice} [slice] - the slice just worked on; !slice => terminate\n * @param {boolean} [verifySandboxIsNotTerminated=true] - if true, check sandbox is not already terminated\n */\n returnSandbox (sandbox, slice, verifySandboxIsNotTerminated=true) {\n if (!slice || slice.failed || sandbox.isTerminated) {\n \n this.removeSandbox(sandbox);\n \n if (!sandbox.isTerminated) {\n debugging('supervisor') && console.log(`Supervisor.returnSandbox: Terminating ${sandbox.identifier}${slice ? `~${slice.identifier}` : ''}, # of sandboxes ${this.sandboxes.length}`);\n sandbox.terminate(false);\n } else {\n debugging('supervisor') && console.log(`Supervisor.returnSandbox: Already terminated ${sandbox.identifier}${slice ? `~${slice.identifier}` : ''}, # of sandboxes ${this.sandboxes.length}`);\n // XXXpfr: April 13, 2022\n // I'm trying to understand and control when sandboxes get terminated.\n // Because of races and random worker crashes, it is impossible to not try to terminate a sandbox more than once.\n // But at some places where returnSandbox is we shouldn't see this behavior, hence this exception.\n // If I don't fix this is the next 30 days or I forget, please delete this exception.\n if (false)\n {}\n }\n }\n }\n\n /**\n * Terminates sandboxes, in order of creation, when the total started sandboxes exceeds the total allowed sandboxes.\n *\n * @returns {Promise<void>}\n */\n pruneSandboxes () {\n let numOver = this.sandboxes.length - (dcpConfig.worker.maxAllowedSandboxes + this.maxWorkingSandboxes);\n if (numOver <= 0) return;\n \n // Don't kill readied sandboxes while creating readied sandboxes.\n for (let index = 0; index < this.readiedSandboxes.length; ) {\n const sandbox = this.readiedSandboxes[index];\n // If the sandbox is allocated, advance to the next one in the list.\n if (sandbox.allocated) {\n index++;\n continue;\n }\n // Otherwise, remove this sandbox but look at the same array index in the next loop.\n debugging('supervisor') && console.log(`pruneSandboxes: Terminating readied sandbox ${sandbox.identifier}`);\n this.readiedSandboxes.splice(index, 1);\n this.returnSandbox(sandbox);\n\n if (--numOver <= 0) break;\n }\n\n if (numOver <= 0) return;\n for (let index = 0; index < this.assignedSandboxes.length; ) {\n const sandbox = this.assignedSandboxes[index];\n // If the sandbox is allocated, advance to the next one in the list.\n if (sandbox.allocated) {\n index++;\n continue;\n }\n // Otherwise, remove this sandbox but look at the same array index in the next loop.\n debugging('supervisor') && console.log(`pruneSandboxes: Terminating assigned sandbox ${sandbox.identifier}`);\n this.assignedSandboxes.splice(index, 1);\n this.returnSandbox(sandbox);\n\n if (--numOver <= 0) break;\n }\n }\n \n /**\n * Basic watch dog to check if there are idle sandboxes and\n * attempts to nudge the supervisor to feed them work.\n *\n * Run in an interval created in @constructor .\n * @returns {Promise<void>}\n */\n async watchdog () {\n if (!this.watchdogState)\n this.watchdogState = {};\n\n // Every 5 minutes, report progress of all working slices to the scheduler\n if (Date.now() > ((this.lastProgressReport || 0) + sandboxTuning.progressReportInterval)) {\n // console.log('454: Assembling progress update...');\n this.lastProgressReport = Date.now();\n\n //\n // Note: this.slices is the disjoint union of:\n // this.allocatedSlices, \n // this.queuedSlices, \n // this.slices.filter(slice => !slice.isUnassigned) .\n // When a slice is not in these 3 arrays, the slice is lost.\n //\n \n const currentLostSlices = this.slices.filter(slice => slice.isUnassigned \n && this.queuedSlices.indexOf(slice) === -1\n && this.allocatedSlices.indexOf(slice) === -1);\n\n if (currentLostSlices.length > 0) {\n this.lostSlices.push(...currentLostSlices);\n // Try to recover.\n // Needs more work and testing.\n // Test when we can come up with a decent lost slice repro case.\n // --> this.queuedSlices.push(...currentLostSlices);\n }\n\n if (this.lostSlices.length > 0) {\n if (true) { // Keep this on for awhile, until we know lost slices aren't happening.\n console.warn('Supervisor.watchdog: Found lost slices!');\n for (const slice of this.lostSlices)\n console.warn('\\t', slice.identifier);\n }\n this.lostSlices = this.lostSlices.filter(slice => slice.isUnassigned);\n }\n\n const slices = [];\n this.queuedSlices.forEach(slice => {\n assert(slice && slice.sliceNumber > 0);\n addToSlicePayload(slices, slice, sliceStatus.scheduled);\n });\n\n this.allocatedSlices.forEach(slice => {\n assert(slice && slice.sliceNumber > 0);\n addToSlicePayload(slices, slice, 'progress'); // Beacon.\n });\n\n if (slices.length) {\n // console.log('471: sending progress update...');\n const progressReportPayload = {\n worker: this.workerOpaqueId,\n slices,\n };\n\n this.resultSubmitterConnection.send('status', progressReportPayload)\n .catch(error => {\n debugging('supervisor') && console.error('479: Failed to send status update:', error/*.message*/);\n this.resultSubmitterMessageQueue.push({ operation: 'status', data: progressReportPayload })\n this.resultSubmitterConnection.close();\n });\n }\n }\n\n if (this.worker.working) {\n if (this.unallocatedSpace > 0) {\n await this.work().catch(err => {\n if (!this.watchdogState[err.code || '0'])\n this.watchdogState[err.code || '0'] = 0;\n if (Date.now() - this.watchdogState[err.code || '0'] > ((dcpConfig.worker.watchdogLogInterval * timeDilation || 120) * 1000))\n console.error('301: Failed to start work:', err);\n this.watchdogState[err.code || '0'] = Date.now();\n });\n }\n\n this.pruneSandboxes();\n }\n }\n\n /**\n * Gets the logical and physical number of cores and also\n * the total number of sandboxes the worker is allowed to run\n *\n */\n getStatisticsCPU() {\n if (DCP_ENV.isBrowserPlatform) {\n return {\n worker: this.workerOpaqueId,\n lCores: window.navigator.hardwareConcurrency,\n pCores: dcpConfig.worker.pCores || window.navigator.hardwareConcurrency,\n sandbox: this.maxWorkingSandboxes\n }\n }\n\n return {\n worker: this.workerOpaqueId,\n lCores: requireNative('os').cpus().length,\n pCores: requireNative('physical-cpu-count'),\n sandbox: this.maxWorkingSandboxes\n }\n }\n\n /**\n * Returns the number of unallocated sandbox slots to send to fetchTask.\n *\n * @returns {number}\n */\n numberOfAvailableSandboxSlots() {\n let numCores;\n if (this.options.priorityOnly && this.options.jobAddresses.length === 0) {\n numCores = 0;\n } else if (this.queuedSlices.length > 1) {\n // We have slices queued, no need to fetch\n numCores = 0;\n } else {\n // The queue is almost empty (there may be 0 or 1 element), fetch a full task.\n // The task is full, in the sense that it will contain slices whose\n // aggregate execution time is this.maxWorkingSandboxes * 5-minutes.\n // However, there can only be this.unallocatedSpace # of long slices.\n // Thus we need to know whether the last slice in this.queuedSlices is long or not.\n // (A long slice has estimated execution time >= 5-minutes.)\n const longSliceCount = (this.queuedSlices.length > 0 && this.queuedSlices[0].isLongSlice) ? 1 : 0;\n numCores = this.unallocatedSpace - longSliceCount;\n }\n return numCores;\n }\n\n /**\n * Call to start doing work on the network.\n * This is the one place where requests to fetch new slices are made.\n * After the initial slices are fetched it calls this.distributeQueuedSlices.\n *\n * @returns {Promise<void>}, unallocatedSpace ${this.unallocatedSpace}\n */\n async work()\n {\n // When inside matchSlicesWithSandboxes, don't reenter Supervisor.work to fetch new work or create new sandboxes.\n if (this.matching) {\n // Interesting and noisy.\n // debugging('supervisor') && console.log(`Supervisor.work: Do not interleave work, fetch or matching slices with sandboxes: queuedSlices ${this.queuedSlices.length}, unallocatedSpace ${this.unallocatedSpace}, matching ${this.matching}, fetching ${this.isFetchingNewWork}`);\n return Promise.resolve();\n }\n\n await this.setDefaultIdentityKeystore();\n\n // Instantiate connections that don't exist.\n this.instantiateAllConnections();\n\n const numCores = this.numberOfAvailableSandboxSlots();\n\n debugging() && console.log(`Supervisor.work: Try to get ${numCores} slices in working sandboxes, unallocatedSpace ${this.unallocatedSpace}, queued slices ${this.queuedSlices.length}, # of sandboxes ${this.sandboxes.length}, matching ${this.matching}, fetching: ${this.isFetchingNewWork}`);\n \n // Fetch a new task if we have no more slices queued, then start workers\n try {\n if (numCores > 0 && !this.isFetchingNewWork) {\n this.isFetchingNewWork = true;\n\n /**\n * This will only ready sandboxes up to a total count of\n * maxWorkingSandboxes (in any state). It is not possible to know the\n * actual number of sandboxes required until we have the slices because we\n * may have sandboxes assigned for the slice's job already.\n *\n * If the evaluator cannot start (ie. if the evalServer is not running),\n * then the while loop will keep retrying until the evalServer comes online\n */\n if (this.maxWorkingSandboxes > this.sandboxes.length) {\n // Note: The old technique had \n // while (this.maxWorkingSandboxes > this.sandboxes.length) {....\n // and sometimes we'd get far too many sandboxes, because it would keep looping while waiting for\n // this.readySandboxes(this.maxWorkingSandboxes - this.sandboxes.length);\n // to construct the rest of the sandboxes. The fix is to only loop when the 1st \n // await this.readySandboxes(1) \n // is failing.\n let needFirstSandbox = true;\n while (needFirstSandbox) {\n debugging('supervisor') && console.log(`Supervisor.work: ready 1 sandbox, # of sandboxes ${this.sandboxes.length}, matching ${this.matching}, fetching ${this.isFetchingNewWork}`);\n await this.readySandboxes(1)\n .then(() => {\n debugging('supervisor') && console.log(`Supervisor.work: ready ${this.maxWorkingSandboxes - this.sandboxes.length} sandbox(es), # of sandboxes ${this.sandboxes.length}, matching ${this.matching}, fetching ${this.isFetchingNewWork}`);\n this.readySandboxes(this.maxWorkingSandboxes - this.sandboxes.length);\n needFirstSandbox = false;\n }).catch(error => {\n console.warn('906: failed to ready sandboxes; will retry', error.code, error.message);\n });\n }\n }\n\n /**\n * Temporary change: Assign the capabilities of one of readied sandboxes\n * before fetching slices from the scheduler.\n *\n * TODO: Remove this once fetchTask uses the capabilities of every\n * sandbox to fetch slices.\n */\n if (!this.capabilities) {\n this.capabilities = this.sandboxes[0].capabilities;\n this.emit('capabilitiesCalculated', this.capabilities);\n }\n\n if (DCP_ENV.isBrowserPlatform && this.capabilities.browser)\n this.capabilities.browser.chrome = DCP_ENV.isBrowserChrome;\n\n const fetchTimeout = setTimeout(() => {\n console.warn(`679: Fetch exceeded timeout, will reconnect at next watchdog interval`);\n \n this.taskDistributorConnection.close('Fetch timed out', Math.random() > 0.5).catch(error => {\n console.error(`931: Failed to close task-distributor connection`, error);\n });\n this.resultSubmitterConnection.close('Fetch timed out', Math.random() > 0.5).catch(error => {\n console.error(`920: Failed to close result-submitter connection`, error);\n });\n this.isFetchingNewWork = false;\n this.instantiateAllConnections();\n }, 3 * 60 * 1000); // max out at 3 minutes to fetch\n\n // ensure result submitter and task distributor connections before fetching tasks\n try\n {\n await this.resultSubmitterConnection.keepalive();\n await this.taskDistributorConnection.keepalive();\n }\n catch (e)\n {\n console.error('Failed to connect to result submitter, refusing to fetch slices. Will try again at next fetch cycle.')\n debugging('supervisor') && console.log(`Error: ${e}`);\n this.isFetchingNewWork = false; // <-- done in the `finally` block, below\n clearTimeout(fetchTimeout);\n this.taskDistributorConnection.close('Failed to connect to result-submitter', true).catch(error => {\n console.error(`939: Failed to close task-distributor connection`, error);\n });\n this.resultSubmitterConnection.close('Failed to connect to result-submitter', true).catch(error => {\n console.error(`942: Failed to close result-submitter connection`, error);\n });\n return Promise.resolve();\n }\n await this.fetchTask(numCores).finally(() => {\n clearTimeout(fetchTimeout);\n this.isFetchingNewWork = false;\n });\n }\n\n this.distributeQueuedSlices().then(() => debugging('supervisor') && 'supervisor: finished distributeQueuedSlices()').catch((e) => {\n // We should never get here, because distributeQueuedSlices was changed\n // to try to catch everything and return slices and sandboxes.\n // If we do catch here it may mean a slice was lost. \n console.error('Supervisor.work catch handler for distributeQueuedSlices.', e);\n });\n // No catch(), because it will bubble outward to the caller\n } finally {\n }\n }\n\n /**\n * Generate the workerComputeGroups property of the requestTask message. \n * \n * Concatenate the compute groups object from dcpConfig with the list of compute groups\n * from the supervisor, and remove the public group if accidentally present. Finally,\n * we transform joinSecrets/joinHash into joinHashHash for secure transmission.\n *\n * @note computeGroup objects with joinSecrets are mutated to record their hashes. This\n * affects the supervisor options and dcpConfig. Re-adding a joinSecret property\n * to one of these will cause the hash to be recomputed.\n */\n generateWorkerComputeGroups()\n {\n var computeGroups = Object.values(dcpConfig.worker.computeGroups || {});\n if (this.options.computeGroups)\n computeGroups = computeGroups.concat(this.options.computeGroups);\n computeGroups = computeGroups.filter(group => group.id !== constants.computeGroups.public.id);\n const hashedComputeGroups = [];\n for (const group of computeGroups)\n {\n const groupCopy = Object.assign({}, group);\n if ((group.joinSecret || group.joinHash) && (!group.joinHashHash || this.lastDcpsid !== this.taskDistributorConnection.dcpsid))\n {\n let joinHash;\n if (group.joinHash) {\n joinHash = group.joinHash.replace(/\\s+/g, ''); // strip whitespace\n } else {\n joinHash = calculateJoinHash(groupCopy);\n } \n\n groupCopy.joinHashHash = hash.calculate(hash.eh1, joinHash, this.taskDistributorConnection.dcpsid);\n delete groupCopy.joinSecret;\n delete groupCopy.joinHash;\n debugging('computeGroups') && console.debug(`Calculated joinHash=${joinHash} for`, groupCopy);\n }\n hashedComputeGroups.push(groupCopy);\n }\n this.lastDcpsid = this.taskDistributorConnection.dcpsid;\n debugging('computeGroups') && console.debug('Requesting ', computeGroups.length, 'non-public groups for session', this.lastDcpsid);\n return hashedComputeGroups;\n }\n\n /**\n * Remove all unreferenced jobs in `this.cache`.\n *\n * @param {any[]} newJobs - Jobs that should not be removed from\n * `this.cache`.\n */\n cleanJobCache(newJobs = []) {\n /* Delete all jobs in the supervisorCache that are not represented in this newJobs,\n * or in this.queuedSlices, or there is no sandbox assigned to these jobs.\n * Note: There can easily be 200+ places to check; using a lookup structure to maintain O(n).\n */\n if (this.cache.jobs.length > 0) {\n const jobAddressMap = {};\n Object.keys(newJobs).forEach(jobAddress => { jobAddressMap[jobAddress] = 1; });\n this.slices.forEach(slice => { if (!jobAddressMap[slice.jobAddress]) jobAddressMap[slice.jobAddress] = 1; });\n this.cache.jobs.forEach(jobAddress => {\n if (!jobAddressMap[jobAddress]) {\n this.cache.remove('job', jobAddress);\n // Remove and return the corresponding sandboxes from this.sandboxes.\n const deadSandboxes = this.sandboxes.filter(sb => sb.jobAddress === jobAddress);\n if (deadSandboxes.length > 0) {\n deadSandboxes.forEach(sandbox => { this.returnSandbox(sandbox); });\n debugging('supervisor') && console.log(`Supervisor.fetchTask: Deleting job ${jobAddress} from cache and assigned sandboxes ${deadSandboxes.map(s => s.id)}, # of sandboxes ${this.sandboxes.length}.`);\n }\n }\n });\n }\n }\n\n /**\n * Fetches a task, which contains job information and slices for sandboxes and\n * manages events related to fetching tasks so the UI can more clearly display\n * to user what is actually happening.\n * @param {number} numCores\n * @returns {Promise<void>} The requestTask request, resolve on success, rejects otherwise.\n * @emits Supervisor#fetchingTask\n * @emits Supervisor#fetchedTask\n */\n async fetchTask(numCores) {\n\n // Don't reenter\n if (this.matching || numCores <= 0) {\n // Interesting and noisy.\n debugging('supervisor') && console.log(`Supervisor.fetchTask: Do not nest work, fetch or matching slices with sandboxes: queuedSlices ${this.queuedSlices.length}, unallocatedSpace ${this.unallocatedSpace}, matching ${this.matching}, fetching ${this.isFetchingNewWork}, numCores ${numCores}`);\n return Promise.resolve();\n }\n\n //\n // Oversubscription mitigation.\n // Update when there are less available sandbox slots than numCores.\n const checkNumCores = this.numberOfAvailableSandboxSlots();\n if (numCores > checkNumCores) numCores = checkNumCores;\n if (numCores <= 0) return Promise.resolve();\n\n this.emit('fetchingTask');\n debugging('supervisor') && console.debug('supervisor: fetching task');\n const requestPayload = {\n numCores,\n coreStats: this.getStatisticsCPU(),\n numGPUs: this.defaultMaxGPUs,\n capabilities: this.capabilities,\n paymentAddress: this.paymentAddress,\n jobAddresses: this.options.jobAddresses || [], // force array; when set, only fetches slices for these jobs\n localExec: this.options.localExec,\n workerComputeGroups: this.generateWorkerComputeGroups(),\n minimumWage: dcpConfig.worker.minimumWage || this.options.minimumWage,\n readyJobs: [ /* list of jobs addresses XXXwg */ ],\n previouslyWorkedJobs: this.ringBufferofJobs.buf, //Only discrete jobs\n rejectedJobs: this.rejectedJobs,\n };\n // workers should be part of the public compute group by default\n if (!booley(dcpConfig.worker.leavePublicGroup) && !booley(this.options.leavePublicGroup) && (!requestPayload.localExec))\n requestPayload.workerComputeGroups.push(constants.computeGroups.public);\n debugging('computeGroups') && console.log(`Fetching work for ${requestPayload.workerComputeGroups.length} ComputeGroups: `, requestPayload.workerComputeGroups);\n debugging('supervisor') && console.log(`fetchTask wants ${numCores} slice(s), unallocatedSpace ${this.unallocatedSpace}, queuedSlices ${this.queuedSlices.length}`);\n try {\n debugging('requestTask') && console.debug('fetchTask: requestPayload', requestPayload);\n\n let result = await this.taskDistributorConnection.send('requestTask', requestPayload).catch((error) => {\n debugging('supervisor') && console.error(`Unable to request task from scheduler: ${error}. Will try again on a new connection.`);\n this.taskDistributorConnection.close(error, true);\n throw error; /* caught below */\n });\n let responsePayload = result.payload; \n\n if (!result.success) {\n debugging() && console.log('Task fetch failure; request=', requestPayload);\n debugging() && console.log('Task fetch failure; response=', result.payload);\n throw new DCPError('Unable to fetch task for worker', responsePayload);\n }\n\n const sliceCount = responsePayload.body.task.length || 0;\n\n /**\n * The fetchedTask event fires when the supervisor has finished trying to\n * fetch work from the scheduler (task-manager). The data emitted is the\n * number of new slices to work on in the fetched task.\n *\n * @event Supervisor#fetchedTask\n * @type {number}\n */\n this.emit('fetchedTask', sliceCount);\n\n if (sliceCount < 1) {\n return Promise.resolve();\n }\n\n /**\n * DCP-1698 Send auth msg with tasks to worker, then validate authority of worker to send slice info back to scheduler.\n * payload structure: { owner: this.address, signature: signature, auth: messageLightWeight, body: messageBody };\n * messageLightWeight: { workerId: worker, jobSlices, schedulerId, jobCommissions }\n * messageBody: { newJobs: await getNewJobsForTask(dbScheduler, task, request), task }\n */\n const { body, ...authorizationMessage } = responsePayload;\n const { newJobs, task } = body;\n assert(newJobs); // It should not be possible to have !newJobs -- we throw on !success.\n \n /*\n * Ensure all jobs received from the scheduler are:\n * 1. If we have specified specific jobs the worker may work on, the received jobs are in the specified job list\n * 2. If we are in localExec, at most 1 unique job type was received (since localExec workers are designated for only\n * one job)\n * If the received jobs are not within these parameters, stop the worker since the scheduler cannot be trusted at that point.\n */\n if ((this.options.jobAddresses.length && !Object.keys(newJobs).every((ele) => this.options.jobAddresses.includes(ele)))\n || (this.options.localExec && Object.keys(newJobs).length > 1))\n {\n console.error(\"Worker received slices it shouldn't have. Rejecting the work and stopping.\");\n process.exit(1);\n }\n\n debugging() && console.log(`Supervisor.fetchTask: task: ${task.length}/${numCores}, jobs: ${Object.keys(newJobs).length}, authSlices: ${compressJobMap(authorizationMessage.auth.authSlices, true /* skipFirst*/, this.addressTruncationLength /* digits*/)}`);\n // Delete all jobs in the supervisorCache that are not represented in this task,\n // or in this.queuedSlices, or there is no sandbox assigned to these jobs.\n this.cleanJobCache(newJobs);\n\n for (const jobAddress of Object.keys(newJobs))\n if (!this.cache.cache.job[jobAddress])\n this.cache.store('job', jobAddress, newJobs[jobAddress]);\n\n // Memoize authMessage onto the Slice object, this should\n // follow it for its entire life in the worker.\n const tmpQueuedSlices = task.map(taskElement => new Slice(taskElement, authorizationMessage));\n\n // Make sure old stuff is up front.\n // matchSlicesWithSandboxes dequeues this.queuedSlices as follows:\n // slicesToMatch = this.queuedSlices.slice(0, numCores);\n this.slices.push(...tmpQueuedSlices);\n this.queuedSlices.push(...tmpQueuedSlices);\n \n // Populating the ring buffer based on job's discrete property \n Object.values(newJobs).forEach(job => {\n if(job.requirements.discrete && this.ringBufferofJobs.find(element => element === job.address) === undefined) {\n this.ringBufferofJobs.push(job.address);\n }\n });\n \n } catch (error) {\n this.emit('fetchTaskFailed', error);\n debugging('supervisor') && console.debug(`Supervisor.fetchTask failed!: error: ${error}`);\n }\n }\n\n /**\n * For each slice in this.queuedSlices, match with a sandbox in the following order:\n * 1. Try to find an already assigned sandbox in this.assignedSandboxes for the slice's job.\n * 2. Find a ready sandbox in this.readiedSandboxes that is unassigned.\n * 3. Ready a new sandbox and use that.\n *\n * Take great care in assuring sandboxes and slices are uniquely associated, viz.,\n * a given slice cannot be associated with multiple sandboxes and a given sandbox cannot be associated with multiple slices.\n * The lack of such uniqueness has been the root cause of several difficult bugs.\n *\n * Note: When a sandbox is paired with a slice, execution is pending and sandbox.allocated=true and\n * sandbox.slice=slice and sandbox.jobAddress=slice.jobAddress. This is what 'allocated' means.\n * Immediately upon the exit of sandbox.work, sandbox.allocated=false is set and if an exception\n * wasn't thrown, the paired slice is placed in this.assignedSandboxes.\n * Thus from the pov of supervisor, this.allocatedSandboxes is deterministic and this.workingSandboxes is not.\n * Please try to not use this.workingSandboxes. It is deprecated.\n *\n * The input is numCores, this,queuedSlices, this.assignedSandboxes and this.readiedSandboxes.\n * If there are not enough sandboxes, new readied sandboxes will be created using\n * await this.readySandboxes(...)\n * And it is this await boundary that has caused many bugs.\n * We try not to make assumptions about non-local state across the await boundary.\n *\n * @param {number} numCores - The number of available sandbox slots.\n * @param {boolean} [throwExceptions=true] - Whether to throw exceptions when checking for sanity.\n * @returns {Promise<SandboxSlice[]>} Returns SandboxSlice[], may have length zero.\n */\n async matchSlicesWithSandboxes (numCores, throwExceptions = true) {\n\n const sandboxSlices = [];\n if (this.queuedSlices.length === 0 || this.matching || numCores <= 0) {\n // Interesting and noisy.\n // debugging('supervisor') && console.log(`Supervisor.matchSlicesWithSandboxes: Do not nest work, fetch or matching slices with sandboxes: queuedSlices ${this.queuedSlices.length}, unallocatedSpace ${this.unallocatedSpace}, matching ${this.matching}, fetching ${this.isFetchingNewWork}, numCores ${numCores}`);\n return sandboxSlices;\n }\n\n //\n // Oversubscription mitigation.\n // Update when there are less available sandbox slots than numCores.\n // We cannot use this.unallocatedSpace here because its value is artificially low or zero, because in\n // this.distributedQueuedSlices we use the pseudo-mutex trick: this.acquire(howManySandboxSlotsToReserve)/this.release().\n // Note: Do not use this.numberOfCoresReserved outside of a function locked with this.acquire(howManySandboxSlotsToReserve) .\n const checkNumCores = this.numberOfCoresReserved; // # of locked sandbox slots.\n if (numCores > checkNumCores) numCores = checkNumCores;\n if (numCores <= 0) return sandboxSlices;\n\n // Don't ask for more than we have.\n if (numCores > this.queuedSlices.length)\n numCores = this.queuedSlices.length;\n\n debugging('supervisor') && console.log(`matchSlicesWithSandboxes: numCores ${numCores}, queued slices ${this.queuedSlices.length}: assigned ${this.assignedSandboxes.length}, readied ${this.readiedSandboxes.length}, unallocated ${this.unallocatedSpace}, # of sandboxes: ${this.sandboxes.length}`);\n\n if (debugging('supervisor')) {\n dumpSlicesIfNotUnique(this.queuedSlices, 'Warning: this.queuedSlices slices are not unique -- this is ok when slice is rescheduled.');\n dumpSandboxesIfNotUnique(this.readiedSandboxes, 'Warning: this.readiedSandboxes sandboxes are not unique!');\n dumpSandboxesIfNotUnique(this.assignedSandboxes, 'Warning: this.assignedSandboxes sandboxes are not unique!');\n }\n\n // Three functions to validate slice and sandbox.\n function checkSlice(slice, checkAllocated=true) {\n if (!slice.isUnassigned) throw new DCPError(`Slice must be unassigned: ${slice.identifier}`);\n if (checkAllocated && slice.allocated) throw new DCPError(`Slice must not already be allocated: ${slice.identifier}`);\n }\n function checkSandbox(sandbox, isAssigned) {\n if (sandbox.allocated) throw new DCPError(`Assigned sandbox must not be already allocated: ${sandbox.identifier}`);\n if (isAssigned && !sandbox.isAssigned) throw new DCPError(`Assigned sandbox is not marked as assigned: ${sandbox.identifier}`);\n if (!isAssigned && !sandbox.isReadyForAssign) throw new DCPError(`Readied sandbox is not marked as ready for assign: ${sandbox.identifier}`);\n }\n\n // Sanity checks.\n if (throwExceptions) {\n this.assignedSandboxes.forEach(sandbox => { checkSandbox(sandbox, true /* isAssigned*/); });\n this.readiedSandboxes.forEach(sandbox => { checkSandbox(sandbox, false /* isAssigned*/); });\n this.queuedSlices.forEach(slice => { checkSlice(slice); });\n } else {\n this.assignedSandboxes = this.assignedSandboxes.filter(sandbox => !sandbox.allocated && sandbox.isAssigned);\n this.readiedSandboxes = this.readiedSandboxes.filter(sandbox => !sandbox.allocated && sandbox.isReadyForAssign);\n this.queuedSlices = this.queuedSlices.filter(slice => !slice.allocated && slice.isUnassigned);\n }\n\n const sandboxKind = {\n assigned: 0,\n ready: 1,\n new: 2,\n };\n\n const ceci = this;\n /**\n * Auxiliary function to pair a sandbox with a slice and mark the sandbox as allocated.\n * An allocated sandbox is reserved and will not be released until the slice completes execution on the sandbox.\n *\n * @param {Sandbox} sandbox\n * @param {Slice} slice\n * @param {number} kind\n */\n function pair(sandbox, slice, kind) {\n checkSandbox(sandbox, kind === sandboxKind.assigned);\n checkSlice(slice, kind === sandboxKind.assigned);\n slice.allocated = true;\n sandbox.allocated = true;\n sandbox.jobAddress = slice.jobAddress; // So we can know which jobs to not delete from this.cache .\n sandbox.slice = slice;\n sandboxSlices.push({ sandbox, slice });\n if (Supervisor.sliceTiming) slice['pairingDelta'] = Date.now();\n if (debugging('supervisor')) {\n let fragment = 'New readied';\n if (kind === sandboxKind.assigned) fragment = 'Assigned';\n else if (kind === sandboxKind.ready) fragment = 'Readied';\n console.log(`matchSlicesWithSandboxes.pair: ${fragment} sandbox matched ${ceci.dumpSandboxAndSlice(sandbox, slice)}`);\n }\n }\n\n // These three arrays are used to track/store slices and sandboxes,\n // so that when an exception occurs, the following arrays are restored:\n // this.queuedSlices, this.assignedSandboxes, this.realizedSandboxes.\n let slicesToMatch = [];\n let trackAssignedSandboxes = [];\n let trackReadiedSandboxes = [];\n try\n {\n this.matching = true;\n\n let assignedCounter = 0; // How many assigned sandboxes are being used.\n let readyCounter = 0; // How many sandboxes used from the existing this.readiedSandboxes.\n let newCounter = 0; // How many sandboxes that needed to be newly created.\n\n //\n // The Ideas:\n // 1) We match each slice with a sandbox. First we match with assigned sandboxes in the order\n // that they appear in this.queuedSlices. Then we match in-order with existing this.readiedSandboxes\n // Then we match in-order with new new readied sandboxes created through\n // await this.readySandboxes(newCounter, true /* allocateLocalSandboxes*/);\n // This allows us to try different orderings of execution of slices. E.g. Wes suggested\n // trying to execute slices from different jobs with maximal job diversity -- specifically\n // if there are 3 jobs j1,j2,j3, with slices s11, s12 from j1, s21, s22, s23 from j2 and\n // s31, s32 from j3, then we try to schedule, in order s11, s21, s31, s12, s22, s32, s23.\n //\n // 2) Before matching slices with sandboxes, we allocate available assigned and readied sandboxes\n // and if more are needed then we create and allocate new ones.\n //\n // 3) Finally we match slices with sandboxes and return an array of sandboxSlice pairs.\n //\n // Note: The ordering of sandboxSlices only partially corresponds to the order of this.queuedSlices.\n // It's easy to do. When pairing with assigned sandboxes, any slice in this.queuedSlices which doesn't\n // have an assigned sandbox, will add null to the sandboxSlices array. Then when pairing with readied sandboxes,\n // we fill-in the null entries in the sandboxSlices array.\n //\n /** XXXpfr @todo When it is needed, fix the ordering as described above. */\n\n // Get the slices that are being matched.\n slicesToMatch = this.queuedSlices.slice(0, numCores);\n this.queuedSlices = this.queuedSlices.slice(numCores);\n\n debugging('supervisor') && console.log(`matchSlicesWithSandboxes: slicesToMatch ${this.dumpSlices(slicesToMatch)}`);\n\n // Create object map: jobAddress -> sandboxes with sandboxes.jobAddress === jobAddress .\n const jobSandboxMap = toJobMap(this.assignedSandboxes, sandbox => sandbox);\n \n // Create array to hold slices which do not have assigned sandboxes.\n // These slices will need to be paired with existing and possibly new readied sandboxes.\n // Specifically, the sandboxes from existing this.readiedSandboxes and new sandboxes\n // created through await this.readySandboxes(newCounter, true /* allocateLocalSandboxes*/);\n const slicesThatNeedSandboxes = [];\n\n // Pair assigned sandboxes with slices.\n for (const slice of slicesToMatch) {\n const assigned = jobSandboxMap[slice.jobAddress];\n if (assigned && assigned.length > 0) {\n // Pair.\n const sandbox = assigned.pop();\n pair(sandbox, slice, sandboxKind.assigned);\n this.removeElement(this.assignedSandboxes, sandbox);\n // Track.\n trackAssignedSandboxes.push(sandbox);\n assignedCounter++;\n } else {\n // Don't lose track of these slices.\n slice.allocated = true;\n slicesThatNeedSandboxes.push(slice);\n }\n }\n\n // Pair readied sandboxes with slices.\n readyCounter = Math.min(slicesThatNeedSandboxes.length, this.readiedSandboxes.length);\n newCounter = slicesThatNeedSandboxes.length - readyCounter;\n // Track.\n trackReadiedSandboxes = this.readiedSandboxes.slice(0, readyCounter);\n this.readiedSandboxes = this.readiedSandboxes.slice(readyCounter);\n for (const sandbox of trackReadiedSandboxes) {\n // Pair.\n const slice = slicesThatNeedSandboxes.pop();\n pair(sandbox, slice, sandboxKind.ready);\n }\n \n debugging('supervisor') && console.log(`matchSlicesWithSandboxes: assignedCounter ${assignedCounter}, readyCounter ${readyCounter}, newCounter ${newCounter}, numCores ${numCores}`)\n\n // Validate algorithm consistency.\n if (Supervisor.debugBuild && assignedCounter + readyCounter + newCounter !== numCores) {\n // Structured assert.\n throw new DCPError(`matchSlicesWithSandboxes: Algorithm is corrupt ${assignedCounter} + ${readyCounter} + ${newCounter} !== ${numCores}`);\n }\n\n // Here is an await boundary.\n // Accessing non-local data across an await boundary may result in the unexpected.\n\n // Create new readied sandboxes to associate with slicesThatNeedSandboxes.\n if (newCounter > 0) {\n // When allocateLocalSandboxes is true, this.readySandboxes does not place the new sandboxes\n // on this.readiedSandboxes. Hence the new sandboxes are private and nobody else can see them.\n debugging('supervisor') && console.log(`matchSlicesWithSandboxes: creating ${newCounter} new sandboxes, # of sandboxes ${this.sandboxes.length}`);\n const readied = await this.readySandboxes(newCounter, true /* allocateLocalSandboxes*/);\n // Track.\n trackReadiedSandboxes.push(...readied);\n\n for (const sandbox of readied) {\n assert(slicesThatNeedSandboxes.length > 0);\n // Pair\n const slice = slicesThatNeedSandboxes.pop();\n pair(sandbox, slice, sandboxKind.new);\n }\n \n // Put back any extras. There should not be any unless readySandboxes returned less than asked for.\n if (slicesThatNeedSandboxes.length > 0) {\n slicesThatNeedSandboxes.forEach(slice => {\n slice.allocated = false;\n this.queuedSlices.push(slice);\n });\n }\n }\n\n if ( false || debugging()) {\n console.log(`matchSlicesWithSandboxes: Matches: ${ this.dumpSandboxSlices(sandboxSlices) }`);\n this.dumpSandboxSlicesIfNotUnique(sandboxSlices, 'Warning: sandboxSlices; { sandbox, slice } pairs are not unique!');\n }\n } catch (e) {\n // Clear allocations.\n slicesToMatch.forEach(slice => { slice.allocated = false; });\n trackAssignedSandboxes.forEach(sandbox => { sandbox.allocated = false; sandbox.slice = null; });\n trackReadiedSandboxes.forEach(sandbox => { sandbox.allocated = false; sandbox.slice = null; sandbox.jobAddress = null; });\n \n // Filter out redundancies -- there shouldn't be any...\n slicesToMatch = slicesToMatch.filter(slice => this.queuedSlices.indexOf(slice) === -1);\n trackAssignedSandboxes = trackAssignedSandboxes.filter(sb => this.assignedSandboxes.indexOf(sb) === -1);\n trackReadiedSandboxes = trackReadiedSandboxes.filter(sb => this.readiedSandboxes.indexOf(sb) === -1);\n\n // Sanity checks.\n slicesToMatch.forEach(slice => { checkSlice(slice) });\n trackAssignedSandboxes.forEach(sandbox => { checkSandbox(sandbox, true /* isAssigned*/); });\n trackReadiedSandboxes.forEach(sandbox => { checkSandbox(sandbox, false /* isAssigned*/); });\n\n // Restore arrays.\n this.queuedSlices.push(...slicesToMatch);\n this.assignedSandboxes.push(...trackAssignedSandboxes);\n this.readiedSandboxes.push(...trackReadiedSandboxes);\n \n console.error('Error in matchSlicesWithSandboxes: Attempting to recover slices and sandboxes.', e);\n return [];\n } finally {\n this.matching = false;\n }\n\n debugging('supervisor') && console.log(`matchSlicesWithSandboxes: allocated ${sandboxSlices.length} sandboxes, queuedSlices ${this.queuedSlices.length}, unallocatedSpace ${this.unallocatedSpace}, matching ${this.matching}, fetching ${this.isFetchingNewWork}, # of sandboxes: ${this.sandboxes.length}.`);\n\n return sandboxSlices;\n }\n\n disassociateSandboxAndSlice(sandbox, slice) {\n this.returnSandbox(sandbox);\n sandbox.slice = null;\n this.returnSlice(slice, 'EUNCAUGHT');\n }\n\n /**\n * This method will call this.startSandboxWork(sandbox, slice) for each element { sandbox, slice }\n * of the array returned by this.matchSlicesWithSandboxes(availableSandboxes) until all allocated sandboxes\n * are working. It is possible for a sandbox to interleave with calling distributeQueuedSlices and leave a sandbox\n * that is not working. Moreover, this.queuedSlices may be exhausted before all sandboxes are working.\n * @returns {Promise<void>}\n */\n async distributeQueuedSlices () {\n const numCores = this.unallocatedSpace;\n\n // If there's nothing there, or we're reentering, bail out.\n if (this.queuedSlices.length === 0 || numCores <= 0 || this.matching) {\n // Interesting and noisy.\n // debugging('supervisor') && console.log(`Supervisor.distributeQueuedSlices: Do not nest work, fetch or matching slices with sandboxes: queuedSlices ${this.queuedSlices.length}, matching ${this.matching}, fetching ${this.isFetchingNewWork}, numCores ${numCores}`);\n return Promise.resolve();\n }\n\n //\n // Use the pseudo-mutex to prevent uncontrolled interleaving with fetchTask,\n // matchSlicesWithSandboxes and distributeQueuedSlices\n let sandboxSlices;\n this.acquire(numCores);\n try {\n sandboxSlices = await this.matchSlicesWithSandboxes(numCores);\n } finally {\n this.release();\n }\n\n debugging('supervisor') && console.log(`distributeQueuedSlices: ${sandboxSlices.length} sandboxSlices ${this.dumpSandboxSlices(sandboxSlices)}, matching ${this.matching}, fetching ${this.isFetchingNewWork}`);\n\n for (let sandboxSlice of sandboxSlices) {\n\n const { sandbox, slice } = sandboxSlice;\n try {\n if (sandbox.isReadyForAssign) {\n try {\n let timeoutMs = Math.floor(Math.min(+Supervisor.lastAssignFailTimerMs || 0, 10 * 60 * 1000 /* 10m */));\n await a$sleepMs(timeoutMs);\n await this.assignJobToSandbox(sandbox, slice.jobAddress);\n } catch (e) {\n console.error(`Supervisor.distributeQueuedSlices: Could not assign slice ${slice.identifier} to sandbox ${sandbox.identifier}.`);\n if (Supervisor.debugBuild) console.error(`...exception`, e);\n Supervisor.lastAssignFailTimerMs = Supervisor.lastAssignFailTimerMs ? +Supervisor.lastAssignFailTimerMs * 1.25 : Math.random() * 200;\n this.disassociateSandboxAndSlice(sandbox, slice);\n continue;\n }\n }\n\n if (!Supervisor.lastAssignFailTimerMs)\n Supervisor.lastAssignFailTimerMs = Math.random() * 200;\n this.startSandboxWork(sandbox, slice);\n Supervisor.lastAssignFailTimerMs = false;\n\n } catch (e) {\n // We should never get here.\n console.error(`Supervisor.distributeQueuedSlices: Failed to execute slice ${slice.identifier} in sandbox ${sandbox.identifier}.`);\n if (Supervisor.debugBuild) console.error('...exception', e);\n this.disassociateSandboxAndSlice(sandbox, slice);\n }\n }\n }\n\n /**\n *\n * @param {Sandbox} sandbox\n * @param {opaqueId} jobAddress\n * @returns {Promise<void>}\n */\n assignJobToSandbox(sandbox, jobAddress) {\n // Any error will be caught in distributeQueuedSlices.\n return sandbox.assign(jobAddress);\n }\n\n /**\n * Handles reassigning or returning a slice that was rejected by a sandbox.\n * \n * The sandbox will be terminated by this.returnSandbox in finalizeSandboxAndSlice. In this case,\n * if the slice does not have a rejected property already, reassign the slice to a new sandbox\n * and add a rejected property to the slice to indicate it has already rejected once, then set slice = null\n * in the return SandboxSlice so that finalizeSandboxAndSlice won't return slice to scheduler.\n * \n * If the slice rejects with a reason, or has a rejected time stamp (ie. has been rejected once already)\n * then return the slice and all slices from the job to the scheduler and\n * terminate all sandboxes with that jobAddress.\n * @param {Sandbox} sandbox \n * @param {Slice} slice\n * @returns {Promise<SandboxSlice>}\n */\n async handleWorkReject(sandbox, slice, rejectReason) {\n if (!this.rejectedJobReasons[slice.jobAddress])\n this.rejectedJobReasons[slice.jobAddress] = [];\n\n this.rejectedJobReasons[slice.jobAddress].push(rejectReason); // memoize reasons\n\n // First time rejecting without a reason. Try assigning slice to a new sandbox.\n if (rejectReason === 'false' && !slice.rejected) {\n // Set rejected.\n slice.rejected = Date.now();\n // Schedule the slice for execution.\n this.scheduleSlice(slice, true /* placeInTheFrontOfTheQueue*/, false /* noDuplicateExecution*/);\n \n // Null out slice so this.returnSlice will not be called in finalizeSandboxAndSlice.\n // But we still want this.returnSandbox to terminate the sandbox.\n slice = null;\n } else { // Slice has a reason OR rejected without a reason already and got stamped.\n \n // Purge all slices and sandboxes associated with slice.jobAddress .\n this.purgeAllWork(slice.jobAddress);\n // Clear jobAddress from this.cache .\n this.cleanJobCache();\n\n // Add to array of rejected jobs.\n let rejectedJob = {\n address: slice.jobAddress,\n reasons: this.rejectedJobReasons[slice.jobAddress],\n }\n this.rejectedJobs.push(rejectedJob);\n\n // Tell everyone all about it, when allowed.\n if (dcpConfig.worker.allowConsoleAccess || Supervisor.debugBuild)\n {\n if (slice.rejected)\n console.warn(`Supervisor.handleWorkReject: The slice ${slice.identifier} was rejected twice.`);\n else\n console.warn(`Supervisor.handleWorkReject: The slice ${slice.identifier} was rejected with reason ${rejectReason}.`);\n console.warn(' All slices with the same jobAddress returned to the scheduler.');\n console.warn(' All sandboxes with the same jobAddress are terminated.');\n }\n //\n // this.purgeAllWork(jobAddress) terminates all sandboxes with jobAddress,\n // and it also returns to scheduler all slices with jobAddress.\n // Therefore null out slice and sandbox so finalizeSandboxAndSlice doesn't do anything.\n // \n sandbox = null;\n slice = null;\n }\n return { sandbox, slice };\n }\n\n /**\n * Schedule the slice to be executed.\n * If slice is already executing and noDuplicateExecution is true, return the slice with reason.\n * @param {Slice} slice\n * @param {boolean} [placeInTheFrontOfTheQueue=false]\n * @param {boolean} [noDuplicateExecution=true]\n * @param {string} [reason]\n */\n scheduleSlice(slice, placeInTheFrontOfTheQueue = false, noDuplicateExecution = true, reason) {\n // When noDuplicateExecution, if slice is already executing, do nothing.\n let workingSlices = [];\n if (noDuplicateExecution)\n workingSlices = this.allocatedSlices;\n\n if (!workingSlices.indexOf(slice)) {\n // Reset slice state to allow execution.\n slice.status = SLICE_STATUS_UNASSIGNED;\n // Enqueue in the to-be-executed queue.\n if (placeInTheFrontOfTheQueue) this.queuedSlices.unshift(slice);\n else this.queuedSlices.push(slice);\n }\n }\n\n /**\n * Purge all slices and sandboxes with this jobAddress.\n * @param {address} jobAddress\n * @param {boolean} [onlyPurgeQueuedAndAllocated=false]\n */\n purgeAllWork(jobAddress, onlyPurgeQueuedAndAllocated = false) {\n // Purge all slices and sandboxes associated with jobAddress .\n const deadSandboxes = this.sandboxes.filter(sandbox => sandbox.jobAddress === jobAddress);\n\n if (deadSandboxes.length > 0) {\n debugging('supervisor') && console.log(`purgeAllWork(${this.dumpJobAddress(jobAddress)}): sandboxes purged ${deadSandboxes.map(s => s.id)}, # of sandboxes ${this.sandboxes.length}`);\n deadSandboxes.forEach(sandbox => this.returnSandbox(sandbox));\n }\n\n let deadSlices;\n if (onlyPurgeQueuedAndAllocated) {\n deadSlices = this.queuedSlices.filter(slice => slice.jobAddress === jobAddress);\n if (deadSlices.length > 0 || this.allocatedSlices.length > 0)\n debugging('supervisor') && console.log(`purgeAllWork(${this.dumpJobAddress(jobAddress)}): dead queuedSlices ${deadSlices.map(s => s.sliceNumber)}, dead allocatedSlices ${this.allocatedSlices.map(s => s.sliceNumber)}`);\n deadSlices.push(...this.allocatedSlices);\n } else {\n deadSlices = this.slices.filter(slice => slice.jobAddress === jobAddress);\n }\n\n if (deadSlices.length > 0) {\n debugging('supervisor') && console.log(`purgeAllWork(${this.dumpJobAddress(jobAddress)}): slices purged ${deadSlices.map(s => s.sliceNumber)}, # of sandboxes ${this.sandboxes.length}`);\n this.returnSlices(deadSlices);\n this.removeQueuedSlices(deadSlices);\n }\n debugging('supervisor') && console.log(`purgeAllWork(${this.dumpJobAddress(jobAddress)}): Finished: slices ${this.slices.length}, queuedSlices ${this.queuedSlices.length}, assigned ${this.assignedSandboxes.length}, readied ${this.readiedSandboxes.length}, # of sandboxes ${this.sandboxes.length}`);\n }\n\n /**\n * Gives a slice to a sandbox which begins working. Handles collecting\n * the slice result (complete/fail) from the sandbox and submitting the result to the scheduler.\n * It will also return the sandbox to @this.returnSandbox when completed so the sandbox can be re-assigned.\n *\n * @param {Sandbox} sandbox - the sandbox to give the slice\n * @param {Slice} slice - the slice to distribute\n * @returns {Promise<void>} Promise returned from sandbox.run\n */\n async startSandboxWork (sandbox, slice) {\n var startDelayMs, reason = 'unknown';\n\n try {\n slice.markAsWorking();\n } catch (e) {\n // This will occur when the same slice is distributed twice.\n // It is normal because two sandboxes could finish at the same time and be assigned the\n // same slice before the slice is marked as working.\n debugging() && console.debug('startSandboxWork: slice.markAsWorking exception:', e);\n return Promise.resolve();\n }\n\n // sandbox.requiresGPU = slice.requiresGPU;\n // if (sandbox.requiresGPU) {\n // this.GPUsAssigned++;\n // }\n\n if (Supervisor.startSandboxWork_beenCalled)\n startDelayMs = 1000 * (tuning.minSandboxStartDelay + (Math.random() * (tuning.maxSandboxStartDelay - tuning.minSandboxStartDelay)));\n else {\n startDelayMs = 1000 * tuning.minSandboxStartDelay;\n Supervisor.startSandboxWork_beenCalled = true;\n }\n\n try {\n debugging() && console.log(`startSandboxWork: Started ${this.dumpStatefulSandboxAndSlice(sandbox, slice)}, total sandbox count: ${this.sandboxes.length}, matching ${this.matching}, fetching ${this.isFetchingNewWork}`);\n if (Supervisor.sliceTiming) {\n slice['pairingDelta'] = Date.now() - slice['pairingDelta'];\n slice['executionDelta'] = Date.now();\n }\n let result;\n try {\n result = await sandbox.work(slice, startDelayMs);\n } finally {\n sandbox.allocated = false;\n slice.allocated = false;\n }\n if (Supervisor.sliceTiming) {\n slice['executionDelta'] = Date.now() - slice['executionDelta'];\n slice['resultDelta'] = Date.now();\n }\n slice.collectResult(result, true);\n // In watchdog, all sandboxes in working state, have their slice status sent to result submitter.\n // However, this can happen after the sandbox/slice has already sent results\n // to result submitter, in which case, the activeSlices table has already removed the row\n // corresponding to slice and hence is incapable of updating status.\n sandbox.changeWorkingToAssigned();\n this.assignedSandboxes.push(sandbox);\n debugging() && console.log(`startSandboxWork: Finished ${this.dumpStatefulSandboxAndSlice(sandbox, slice)}, total sandbox count: ${this.sandboxes.length}, matching ${this.matching}, fetching ${this.isFetchingNewWork}`);\n } catch(error) {\n let logLevel;\n\n if (error instanceof SandboxError) {\n logLevel = 'warn';\n // The message and stack properties of error objects are not enumerable,\n // so they have to be copied into a plain object this way\n const errorResult = Object.getOwnPropertyNames(error).reduce((o, p) => {\n o[p] = error[p]; return o;\n }, { message: 'Unexpected worker error' });\n slice.collectResult(errorResult, false);\n } else {\n logLevel = 'error';\n // This error was unrelated to the work being done, so just return the slice in the finally block.\n // For extra safety the sandbox is terminated.\n slice.result = null;\n slice.status = SLICE_STATUS_FAILED; /** XXXpfr @todo terminating sandbox? */\n }\n\n let errorString;\n switch (error.errorCode) {\n case 'ENOPROGRESS':\n reason = 'ENOPROGRESS';\n errorString = 'Supervisor.startSandboxWork - No progress error in sandbox.\\n';\n break;\n case 'ESLICETOOSLOW':\n reason = 'ESLICETOOSLOW';\n errorString = 'Supervisor.startSandboxWork - Slice too slow error in sandbox.\\n';\n break;\n case 'EUNCAUGHT':\n reason = 'EUNCAUGHT';\n errorString = `Supervisor.startSandboxWork - Uncaught error in sandbox ${error.message}.\\n`;\n break;\n case 'EFETCH':\n // reason = 'EFETCH'; The status.js processing cannot handle 'EFETCH'\n reason = 'unknown';\n errorString = `Supervisor.startSandboxWork - Could not fetch data: ${error.message}.\\n`;\n break;\n }\n \n const { getenv } = __webpack_require__(/*! ../../common/dcp-env */ \"./src/common/dcp-env.js\");\n // Always display max info under debug builds, otherwise maximal error\n // messages are displayed to the worker, only if both worker and client agree.\n let workerConsole = sandbox.supervisorCache.cache.job[slice.jobAddress].workerConsole;\n const displayMaxInfo = Boolean(getenv('DCP_SUPERVISOR_DEBUG_DISPLAY_MAX_INFO')) || (workerConsole && dcpConfig.worker.allowConsoleAccess);\n\n const errorObject = {\n jobAddress: slice.jobAddress.substr(0,10),\n sliceNumber: slice.sliceNumber,\n sandbox: sandbox.id,\n jobName: sandbox.public ? sandbox.public.name : 'unnamed',\n };\n \n if (error.name === 'EWORKREJECT') {\n error.stack = 'Sandbox was terminated by work.reject()';\n const ss = await this.handleWorkReject(sandbox, slice, error.message);\n sandbox = ss.sandbox; slice = ss.slice;\n }\n\n if (!displayMaxInfo && error.errorCode === 'EUNCAUGHTERROR') {\n console[logLevel](`Supervisor.startSandboxWork - Uncaught error in sandbox, could not compute.\\n`, errorObject);\n } else if (!displayMaxInfo && error.errorCode === 'EFETCH_BAD_ORIGIN') {\n console[logLevel](`Supervisor.startSandboxWork - Could not fetch data: ${error.message}`);\n } else if (!displayMaxInfo && errorString) {\n console[logLevel](errorString, errorObject);\n } else if (!displayMaxInfo && error.name === 'EWORKREJECT') {\n console[logLevel](`Supervisor.startSandboxWork - Sandbox rejected work: ${error.message}`)\n } else {\n if (displayMaxInfo)\n errorObject.stack += '\\n --------------------\\n' + (error.stack.split('\\n').slice(1).join('\\n'));\n console[logLevel](`Supervisor.startSandboxWork - Sandbox failed: ${error.message}\\n`, errorObject);\n }\n } finally {\n await this.finalizeSandboxAndSlice(sandbox, slice, reason);\n }\n }\n\n /**\n * If slice && slice.result, then call await this.recordResult(slice) and this.returnSandbox(sandbox, slice) will have no effect.\n * If slice && !slice.result, then call this.returnSlice(slice, reason) and then this.returnSandbox(sandbox, slice) which terminates sandbox.\n * If !slice && sandbox, then terminate the sandbox with this.returnSandbox(sandbox, slice) .\n * If !slice && !sandbox, then do nothing.\n * @param {Sandbox} [sandbox]\n * @param {Slice} [slice]\n * @param {string} [reason]\n */\n async finalizeSandboxAndSlice(sandbox, slice, reason) {\n debugging('supervisor') && console.log(`finalizeSandboxAndSlice: sandbox ${sandbox ? sandbox.identifier : 'nade'}, slice ${slice ? slice.identifier : 'nade'}`);\n if (slice) {\n if (slice.result) await this.recordResult(slice);\n else this.returnSlice(slice, reason);\n }\n // It is possible that sandbox is already terminated\n // Because sandbox.allocated=false as soon as sandbox.work(...) completes.\n // But the await at or in finalizeSandboxAndSlice may allow pruneSandboxes to slither in.\n if (sandbox) this.returnSandbox(sandbox, slice, false /* verifySandboxIsNotTerminated*/);\n }\n\n /**\n * Terminates sandboxes and returns slices.\n * Sets the working flag to false, call @this.work to start working again.\n * \n * If forceTerminate is true: Terminates all sandboxes and returns all slices.\n * If forceTerminate is false: Terminates non-allocated sandboxes and returns queued slices.\n *\n * @param {boolean} [forceTerminate = true] - true if you want to stop the sandboxes from completing their current slice.\n * @returns {Promise<void>}\n */\n async stopWork (forceTerminate = true) {\n debugging('supervisor') && console.log('stopWork(${forceTerminate}): terminating sandboxes and returning slices to scheduler.');\n if (forceTerminate) {\n while (this.sandboxes.length) {\n this.returnSandbox(this.sandboxes[0], null, false);\n }\n\n await this.returnSlices(this.slices).then(() => {\n this.queuedSlices.length = 0;\n });\n } else {\n // Only terminate idle sandboxes and return only queued slices\n let idleSandboxes = this.sandboxes.filter(w => !w.allocated);\n for (const sandbox of idleSandboxes) {\n this.returnSandbox(sandbox, null, false /* verifySandboxIsNotTerminated*/);\n }\n\n await this.returnSlices(this.queuedSlices).then(() => {\n this.queuedSlices.length = 0;\n });\n\n await new Promise((resolve, reject) => {\n let sandboxesRemaining = this.allocatedSandboxes.length;\n if (sandboxesRemaining === 0)\n {\n resolve();\n }\n // Resolve and finish work once all sandboxes have finished submitting their results.\n this.on('submitFinished', () => {\n sandboxesRemaining--;\n if (sandboxesRemaining === 0)\n {\n console.log('All sandboxes empty, stopping worker and closing all connections');\n resolve();\n }\n });\n });\n }\n\n if (this.resultSubmitterConnection) {\n this.resultSubmitterConnection.off('close', this.openResultSubmitterConn);\n this.resultSubmitterConnection.close();\n this.resultSubmitterConnection = null;\n }\n\n if (this.taskDistributorConnection) {\n this.taskDistributorConnection.off('close', this.openTaskDistributorConn);\n this.taskDistributorConnection.close();\n this.taskDistributorConnection = null;\n }\n\n if (this.packageManagerConnection) {\n this.packageManagerConnection.off('close', this.openPackageManagerConn);\n this.packageManagerConnection.close();\n this.packageManagerConnection = null;\n }\n\n if (this.eventRouterConnection) {\n this.eventRouterConnection.off('close', this.openEventRouterConn);\n this.eventRouterConnection.close();\n this.eventRouterConnection = null;\n }\n\n this.emit('stop');\n }\n\n /**\n * Takes a slice and returns it to the scheduler to be redistributed.\n * Usually called when an exception is thrown by sandbox.work(slice, startDelayMs) .\n * Or when the supervisor tells it to forcibly stop working.\n *\n * @param {Slice} slice - The slice to return to the scheduler.\n * @param {string} [reason] - Optional reason for the return: 'ENOPROGRESS', 'EUNCAUGHT', 'ESLICETOOSLOW', 'unknown'.\n * @returns {Promise<*>} - Response from the scheduler.\n */\n returnSlice (slice, reason) {\n // When sliceNumber === 0 don't send a status message.\n if (slice.sliceNumber === 0) return Promise.resolve();\n \n debugging() && console.log(`Supervisor.returnSlice: Returning slice ${slice.identifier} with reason ${reason}.`);\n \n const payload = slice.getReturnMessagePayload(this.workerOpaqueId, reason);\n try\n {\n return this.resultSubmitterConnection.send('status', payload) /* resultSubmitterConnection can be null if worker is stopped */\n .then(response => {\n return response;\n }).catch(error => {\n debugging('supervisor') && console.error('Failed to return slice', {\n sliceNumber: slice.sliceNumber,\n jobAddress: slice.jobAddress,\n status: slice.status,\n error,\n }, 'Will try again on a new connection.');\n this.resultSubmitterMessageQueue.push({ operation: 'status', data: payload });\n this.resultSubmitterConnection.close();\n });\n }\n catch (error)\n {\n debugging('supervisor') && console.error(`Failed to return slice ${slice.identifier}, no connection to result submitter:`, error);\n }\n }\n\n /**\n * Bulk-return multiple slices, possibly for assorted jobs.\n * Returns slices to the scheduler to be redistributed.\n * Called in the sandbox terminate handler and purgeAllWork(jobAddress)\n * and stopWork(forceTerminate).\n *\n * @param {Slice[]} slices - The slices to return to the scheduler.\n * @returns {Promise<void>} - Response from the scheduler.\n */\n async returnSlices(slices) {\n if (!slices || !slices.length) return Promise.resolve();\n \n const slicePayload = [];\n slices.forEach(slice => { addToReturnSlicePayload(slicePayload, slice); });\n this.removeSlices(slices);\n\n debugging('supervisor') && console.log(`Supervisor.returnSlices: Returning slices ${this.dumpSlices(slices)}.`);\n\n return this.resultSubmitterConnection.send('status', {\n worker: this.workerOpaqueId,\n slices: slicePayload,\n }).then(response => {\n return response;\n }).catch(error => {\n const errorInfo = slices.map(slice => slice.identifier);\n debugging('supervisor') && console.error('Failed to return slice(s)', { errorInfo, error }, 'Will try again on new connection.');\n this.resultSubmitterMessageQueue.push({ operation: 'status', data: { worker: this.workerOpaqueId, slices: slicePayload } })\n this.resultSubmitterConnection.close();\n // Just in case the caller is expecing a DCP response\n return { success: false, payload: {} };\n });\n }\n\n /**\n * Submits the slice results to the scheduler, either to the\n * work submit or fail endpoints based on the slice status.\n * Then remove the slice from the @this.slices cache.\n *\n * @param {Slice} slice - The slice to submit.\n * @returns {Promise<void>}\n */\n async recordResult (slice) {\n // It is possible for slice.result to be undefined when there are upstream errors.\n if ( !(slice && slice.result))\n throw new Error(`recordResult: slice.result is undefined for slice ${slice.identifier}. This is ok when there are upstream errors.`);\n\n debugging('supervisor') && console.log(`supervisor: recording result for slice ${slice.identifier}.`);\n\n const jobAddress = slice.jobAddress;\n const sliceNumber = slice.sliceNumber;\n const authorizationMessage = slice.getAuthorizationMessage();\n\n /* @see result-submitter::result for full message details */\n const metrics = { GPUTime: 0, CPUTime: 0, CPUDensity: 0, GPUDensity: 0, total: 0 };\n const payloadData = {\n slice: sliceNumber,\n job: jobAddress,\n worker: this.workerOpaqueId,\n paymentAddress: this.paymentAddress,\n metrics,\n authorizationMessage,\n }\n\n const timeReport = slice.timeReport;\n if (timeReport && timeReport.total > 0) {\n metrics.GPUTime = timeReport.webGL;\n metrics.CPUTime = timeReport.CPU;\n metrics.CPUDensity = metrics.CPUTime / timeReport.total;\n metrics.GPUDensity = metrics.GPUTime / timeReport.total;\n metrics.total = timeReport.total;\n metrics.CPUTime = 1 + Math.floor(metrics.CPUTime);\n if (metrics.GPUTime > 0)\n metrics.GPUTime = 1 + Math.floor(metrics.GPUTime);\n }\n \n this.emit('submittingResult');\n\n if (!slice.isFinished)\n throw new Error('Cannot record result for slice that is not finished');\n\n if (slice.resultStorageType === 'pattern') { /* This is a remote-storage slice. */\n const remoteResult = await this.sendResultToRemote(slice);\n payloadData.result = encodeDataURI(JSON.stringify(remoteResult));\n } else {\n payloadData.result = encodeDataURI(slice.result.result); /* XXXwg - result.result is awful */\n }\n debugging('supervisor') && console.log('Supervisor.recordResult: payloadData.result', payloadData.result.slice(0, 512));\n\n try {\n if (slice.completed) {\n\n /* work function returned a result */\n let resp = await this.resultSubmitterConnection.send(\n 'result',\n payloadData,\n )\n \n if (!resp.success) {\n if (resp.payload.code === 'DCPS-01002') { /* result submitter cannot connect to event router; try again */\n try {\n resp = await this.resendResult(payloadData)\n } catch (error) {\n debugging('supervisor') && console.error(`supervisor - failed to submit result for job ${jobAddress} after ${payloadData.sendRetries} attempts`)\n throw error;\n }\n }\n else\n throw resp.payload;\n }\n\n if (false) {}\n\n const receipt = {\n accepted: true,\n payment: resp.payload.slicePaymentAmount,\n };\n this.emit('submittedResult', resp.payload);\n this.emit('dccCredit', receipt);\n } else {\n /* slice did not complete for some reason */\n \n // If the slice from a job never completes and the job address exists in the ringBufferofJobs, \n // then we remove it to allow for another slice (from the same job) to be obtained by fetchTask\n this.ringBufferofJobs.buf = this.ringBufferofJobs.filter(element => element !== jobAddress);\n \n await this.returnSlice(slice);\n }\n } catch(error) {\n console.info(`1014: Failed to submit results for slice ${payloadData.slice} of job ${payloadData.job}`, error);\n this.emit('submitSliceFailed', error);\n } finally {\n this.emit('submitFinished');\n // Remove the slice from the slices array.\n this.removeSlice(slice);\n if (Supervisor.sliceTiming) {\n slice['resultDelta'] = Date.now() - slice['resultDelta'];\n console.log(`recordResult(${slice['pairingDelta']}, ${slice['executionDelta']}, ${slice['resultDelta']}): Completed slice ${slice.identifier}.`);\n } else\n debugging('supervisor') && console.log(`recordResult: Completed slice ${slice.identifier}.`);\n }\n }\n\n /**\n * Send a work function's result to a server that speaks our DCP Remote Data Server protocol.\n * The data server dcp-rds is been implemented in https://gitlab.com/Distributed-Compute-Protocol/dcp-rds .\n *\n * @param {Slice} slice - Slice object whose result we are sending.\n * @returns {Promise<object>} - Object of the form { success: true, href: 'http://127.0.0.1:3521/methods/download/jobs/34/result/10' } .\n * @throws When HTTP status not in the 2xx range.\n */\n async sendResultToRemote(slice) {\n const postParams = {\n ...slice.resultStorageParams\n };\n \n const patternParams = {\n slice: slice.sliceNUmber,\n job: slice.jobAddress\n };\n \n /**\n * @todo Future Work: we need to pass the pattern parameters with the job details into the supervisor. \n * We do not have all the information (necessarily) to calculate them from here. /wg jan 2022\n */\n const sliceResultUri = makeValueURI('pattern', slice.resultStorageDetails, patternParams)\n\n debugging() && console.log('sendResultToRemote sliceResultUri: ', sliceResultUri);\n \n if (this.makeSafeOriginList('sendResults').indexOf(sliceResultUri.origin) === -1)\n throw new Error(`Invalid origin for remote result storage: '${sliceResultUri.origin}'`);\n\n postParams.element = slice.sliceNumber;\n postParams.contentType = 'application/json'; // Currently data will be outputed as a JSON object, @todo: Support file upload.\n\n debugging() && console.log('sendResultToRemote: postParams: ', postParams);\n\n let result = slice.result.result;\n if (result) {\n postParams.content = JSON.stringify(result);\n } else {\n postParams.error = JSON.stringify(slice.error);\n }\n\n debugging('supervisor') && console.log('sendResultToRemote: content: ', (result ? postParams.content : postParams.error).slice(0, 512));\n\n //\n // Notes:\n // 1) In recordResults the response from justFetch is JSON serialized and encodeDataURI is called.\n // payloadData.result = await this.sendResultToRemote(slice);\n // payloadData.result = encodeDataURI(JSON.stringify(payloadData.result));\n // 2) We do further processing after the call to sendResultToRemote in recordResult, because\n // if we did it here there would be a perf hit. When the return value is a promise, it gets\n // folded into sendResultToRemote's main promise. If justFetch's promise wasn't a return value then\n // justFetch would be separately added to the micro-task-queue.\n return await justFetch(sliceResultUri, 'JSON', 'POST', false, postParams);\n }\n}\n\n/**\n * Sandbox has had an error which is not from the work function: kill it\n * and try to redo the slice.\n */\nfunction handleSandboxError(supervisor, sandbox, error) {\n const slice = sandbox.slice;\n\n slice.sandboxErrorCount = (slice.sandboxErrorCount || 0) + 1;\n sandbox.slice = null;\n supervisor.returnSandbox(sandbox); /* terminate the sandbox */\n slice.status = SLICE_STATUS_UNASSIGNED; /* ToT */\n console.warn(`Supervisor.handleSandboxError: Sandbox ${sandbox.identifier}...(${sandbox.public.name}/${slice.sandboxErrorCount}) with slice ${slice.identifier} had error.`, error);\n\n if (slice.sandboxErrorCount < dcpConfig.worker.maxSandboxErrorsPerSlice)\n supervisor.queuedSlices.push(slice);\n else {\n slice.error = error;\n supervisor.returnSlice(slice);\n }\n}\n\n/**\n * Add a slice to the slice payload being built. If a sliceList already exists for the\n * job-status-authMessage tuple, then the slice will be added to that, otherwise a new\n * sliceList will be added to the payload.\n *\n * @param {Object[]} slicePayload - Slice payload being built. Will be mutated in place.\n * @param {Slice} slice - The slice.\n * @param {String} status - Status update, eg. progress or scheduled.\n *\n * @returns {Object[]} mutated slicePayload array\n */\nfunction addToSlicePayload(slicePayload, slice, status) {\n // getAuthorizationMessage helps enforces the equivalence\n // !authorizationMessage <==> sliceNumber === 0\n const authorizationMessage = slice.getAuthorizationMessage();\n if (!authorizationMessage) return;\n\n // Try to find a sliceList in the payload which matches the job, status, and auth message\n let sliceList = slicePayload.find(desc => {\n return desc.job === slice.jobAddress\n && desc.status === status\n && desc.authorizationMessage === authorizationMessage;\n });\n\n // If we didn't find a sliceList, start a new one and add it to the payload\n if (!sliceList) {\n sliceList = {\n job: slice.jobAddress,\n sliceNumbers: [],\n status,\n authorizationMessage,\n };\n slicePayload.push(sliceList);\n }\n\n sliceList.sliceNumbers.push(slice.sliceNumber);\n\n return slicePayload;\n}\n\n/**\n * Add a slice to the returnSlice payload being built. If a sliceList already exists for the\n * job-isEstimation-authMessage-reason tuple, then the slice will be added to that, otherwise a new\n * sliceList will be added to the payload.\n *\n * @param {Object[]} slicePayload - Slice payload being built. Will be mutated in place.\n * @param {Slice} slice - The slice.\n * @param {String} [reason] - Optional reason to further characterize status; e.g. 'ENOPROGRESS', 'EUNCAUGHT', 'ESLICETOOSLOW', 'unknown'.\n *\n * @returns {Object[]} mutated slicePayload array\n */\nfunction addToReturnSlicePayload(slicePayload, slice, reason) {\n // getAuthorizationMessage helps enforces the equivalence\n // !authorizationMessage <==> sliceNumber === 0\n const authorizationMessage = slice.getAuthorizationMessage();\n if (!authorizationMessage) return;\n\n if (!reason) reason = slice.error ? 'EUNCAUGHT' : 'unknown';\n\n // Try to find a sliceList in the payload which matches the job, status, and auth message\n let sliceList = slicePayload.find(desc => {\n return desc.job === slice.jobAddress\n && desc.isEstimationSlice === slice.isEstimationSlice\n && desc.authorizationMessage === authorizationMessage\n && desc.reason === reason;\n });\n\n // If we didn't find a sliceList, start a new one and add it to the payload\n if (!sliceList) {\n sliceList = {\n job: slice.jobAddress,\n sliceNumbers: [],\n status: 'return',\n isEstimationSlice: slice.isEstimationSlice,\n authorizationMessage,\n reason,\n };\n slicePayload.push(sliceList);\n }\n\n sliceList.sliceNumbers.push(slice.sliceNumber);\n\n return slicePayload;\n}\n\n/**\n * Return DCPv4-specific connection options, composed of type-specific, URL-specific, \n * and worker-specific options, any/all of which can override the dcpConfig.dcp.connectOptions.\n * The order of precedence is the order of specificity.\n */\nfunction connectionOptions(url, label) {\n return leafMerge(/* ordered from most to least specific */\n dcpConfig.worker.dcp.connectionOptions.default,\n dcpConfig.worker.dcp.connectionOptions[label],\n dcpConfig.worker.dcp.connectionOptions[url.href]);\n}\n\n/** @type {number | boolean} */\nSupervisor.lastAssignFailTimerMs = false;\n/** @type {boolean} */\nSupervisor.startSandboxWork_beenCalled = false;\n/** @type {boolean} */\nSupervisor.debugBuild = ((__webpack_require__(/*! dcp/common/dcp-build */ \"./src/common/dcp-build.js\").build) === 'debug');\n/**\n * When Supervisor.sliceTiming is set to be true, it displays the timings of a every slice\n * slice['pairingDelta'] = timespan of when slice is paired with sandbox until execution starts\n * slice['executionDelta'] = timespan of execution in sandbox\n * slice['resultDelta'] = timespan of when sandbox finishes executing until recordResult completes.\n * @type {boolean}\n */\nSupervisor.sliceTiming = false;\n\nexports.Supervisor = Supervisor;\n\n\n//# sourceURL=webpack://dcp/./src/dcp-client/worker/supervisor.js?");
4516
+
4517
+ /***/ }),
4518
+
4519
+ /***/ "./src/dcp-client/worker/supervisor2/index.js":
4520
+ /*!****************************************************!*\
4521
+ !*** ./src/dcp-client/worker/supervisor2/index.js ***!
4522
+ \****************************************************/
4523
+ /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
4524
+
4525
+ "use strict";
4526
+ eval("/* provided dependency */ var process = __webpack_require__(/*! ./node_modules/process/browser.js */ \"./node_modules/process/browser.js\");\n/**\n * @file dcp-client/worker/supervisor2/index.js\n * Code managing sandboxes, tasks, jobs, and slices within in a DCP Worker.\n * @author Wes Garland, wes@kingsds.network\n * Paul, paul@kingsds.network\n * @date Dec 2020\n * June 2022\n * @module supervisor\n */\n\n/* global dcpConfig */ // eslint-disable-line no-redeclare\n// @ts-check\n\n\nconst DCP_ENV = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\nconst { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\nconst dcp4 = __webpack_require__(/*! dcp/protocol-v4 */ \"./src/protocol-v4/index.js\");\nconst debugging = (__webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope)('worker');\nconst constants = __webpack_require__(/*! dcp/common/scheduler-constants */ \"./src/common/scheduler-constants.js\");\nconst { setImmediate } = __webpack_require__(/*! dcp/common/dcp-timers */ \"./src/common/dcp-timers.js\");\nconst { EventEmitter } = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\");\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\nconst { Keystore, Address } = __webpack_require__(/*! dcp/dcp-client/wallet */ \"./src/dcp-client/wallet/index.js\");\nconst { assert } = __webpack_require__(/*! dcp/common/dcp-assert */ \"./src/common/dcp-assert.js\");\nconst { localStorage } = __webpack_require__(/*! dcp/common/dcp-localstorage */ \"./src/common/dcp-localstorage.js\");\nconst { DcpURL } = __webpack_require__(/*! dcp/common/dcp-url */ \"./src/common/dcp-url.js\");\nconst RingBuffer = __webpack_require__(/*! dcp/utils/ringBuffer */ \"./src/utils/ringBuffer.js\");\nconst { Synchronizer } = __webpack_require__(/*! dcp/common/concurrency */ \"./src/common/concurrency.js\");\nconst { JobManager } = __webpack_require__(/*! ./job-manager */ \"./src/dcp-client/worker/supervisor2/job-manager.js\");\nconst { Load } = __webpack_require__(/*! ./load */ \"./src/dcp-client/worker/supervisor2/load.js\");\nconst { Sandbox, SandboxError } = __webpack_require__(/*! ./sandbox2 */ \"./src/dcp-client/worker/supervisor2/sandbox2.js\");\nconst { sliceStatus } = __webpack_require__(/*! dcp/common/scheduler-constants */ \"./src/common/scheduler-constants.js\");\nconst hash = __webpack_require__(/*! dcp/common/hash */ \"./src/common/hash.js\");\nconst { calculateJoinHash } = __webpack_require__(/*! dcp/dcp-client/compute-groups */ \"./src/dcp-client/compute-groups/index.js\");\nconst { ModuleCache } = __webpack_require__(/*! ./module-cache */ \"./src/dcp-client/worker/supervisor2/module-cache.js\");\nconst { Inventory, leafMerge, a$sleepMs, ms, pct, generateOpaqueId, booley, compressJobMap, \n toJobMap, truncateAddress, encodeDataURI, makeDataURI, justFetch, stringify } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\n//const { CodeFlow } = require('client-oauth2');\n//const { text } = require('./lang').getLocaleStrings('en_CA'); /** XXXpfr @todo Don't know what to do with localization? */\n\n/** @typedef {import('dcp/dcp-client/wallet/eth').Address} Address */\n/** @typedef {import('dcp/protocol-v4/connection/connection').Connection} Connection */\n/** @typedef {string} opaqueId */ // 22 character base64 string\n/** @typedef {import('..').Worker} Worker */\n/** @typedef {import('..').SupervisorOptions} SupervisorOptions */\n/** @typedef {import('./slice2').Slice} Slice */\n/** @typedef {import('dcp/utils').SliceMessage} SliceMessage */\n\nconst addressTruncationLength = 20;\nconst AWAIT_ALL = false;\n\n//\n// Configs are moving around in dcpConfig and local worker configs, so set up some defaults.\nlet workerTuning = dcpConfig.worker;\nif (!workerTuning) workerTuning = dcpConfig.Supervisor;\nif (!workerTuning || !workerTuning.dcp || !workerTuning.dcp.tuning || !workerTuning.dcp.tuning.watchdogInterval\n || !workerTuning.sandbox || !workerTuning.allowOrigins || !workerTuning.minimumWage || !workerTuning.computeGroups)\n workerTuning = {\n dcp: {\n tuning: { watchdogInterval: 7, minSandboxStartDelay: 0.1, maxSandboxStartDelay: 0.7 },\n connectionOptions: { default: { identityUnlockTimeout: 15 * 60 /* seconds */ } },\n },\n sandbox: { progressReportInterval: 2 * 60 * 100 },\n allowOrigins: { fetchWorkFunctions: [], fetchArguments: [], fetchData: [], sendResults: [], any: [] },\n minimumWage: { CPU: 0, GPU: 0, 'in': 0, out: 0 },\n leavePublicGroup: false,\n pCores: 0,\n computeGroups: {},\n // The following configs are not in dcpConfig or worker configs (yet), but may be specified in local worker configs to override the defaults.\n pruneFrequency: 15 * 1000, // Maxiumum time interval where we check to prune used sandboxes.\n workerSandboxThreshold: 7, // When maxWorkingSandboxes >= workerSandboxThreshold, we allow an extra 25% of assigned sandboxes that won't be pruned.\n cachedJobsThreshold: 12, // Prune the unused job managers >= cachedJobsThreshold.\n};\n\n//\n// Flags for tracing.\n//\nconst selectiveEnable = false;\nconst displayWarnError = false || selectiveEnable;\nconst selectiveDebugging = selectiveEnable || debugging();\nconst debuggingError = false || selectiveDebugging || displayWarnError;\nconst debuggingWarn = false || selectiveDebugging || displayWarnError;\nconst selectiveDebugging2 = selectiveEnable && false || debugging('supervisor');\n\n/** \n * Adjust delay times when debugging.\n * The adjustment for niim is automatic, other debuggers must manually change this value.\n */\nlet timeDilation = 1;\nif (DCP_ENV.platform === 'nodejs')\n{\n /** Make timers 10x slower when running in niim */\n timeDilation = (requireNative('module')._cache.niim instanceof requireNative('module').Module) ? 10 : 1;\n}\n\n//\n// Index to functionality -- search for '_Idx' to toggle through the index.\n//\n// 1) Ctor: Supervisor constructor.\n// 2) Important property-like functions.\n// 3) Dtors: screenSaverDestroy, stopWork, purgeJob.\n// 4) Connection code.\n// 5) Work: Distribute slice to sandboxes.\n// 6) Return slices and sent progress reports to result-submitter-results.\n// 7) Task Distributor (TD): requestTask (Rq) support -- communication with TD.\n// 8) Aggregators from the job managers.\n// 9) Sandbox creation and management.\n// 10) Result-submitter-result support functions.\n// 11) Work reject.\n// 12) Unused functions that we need to review.\n//\n\n// _Idx\n//\n// Ctor: Supervisor constructor.\n//\n\n/** \n * Supervisor constructor\n * \n * A supervisor manages the communication with the scheduler, manages sandboxes, and\n * decides which workload should be sent to which sandboxes when.\n *\n * Start state:\n * - initial\n *\n * Intermediate states:\n * - ready\n * - stopping\n *\n * Terminal states:\n * - stopped\n *\n * Valid transitions:\n * - initial -> ready where that happens \n * - ready -> stopping\n * - stopping -> stopped\n *\n * @param {Worker} worker - The worker that created this instance.\n * @param {SupervisorOptions} options - Options for specifying custom behaviour and tuning,\n */\nfunction Supervisor(worker, options)\n{\n assert(options.identity instanceof Keystore);\n assert(options.paymentAddress instanceof Address);\n\n /**\n * Flag to indicate a debug build.\n * Used when we want to display extra information and do extra checks for developers only.\n * @type {boolean}\n */\n this.debugBuild = ((__webpack_require__(/*! dcp/common/dcp-build */ \"./src/common/dcp-build.js\").build) === 'debug');\n /**\n * When Supervisor.sliceTiming is set to be true, it displays the timings of a every slice\n * slice['queueingDelta'] = timespan of when slice is passed to jobManager.runQueuedSlice until sandbox.work\n * slice['executionDelta'] = timespan of execution in sandbox\n * slice['resultDelta'] = timespan of when sandbox finishes executing until recordResult completes.\n * @type {boolean}\n */\n this.sliceTiming = false;\n /** Used for analyzing the completed results in Supervisor.recordResult. */\n this.resultMap = {};\n\n /** @type {ModuleCache} */\n this.cache = new ModuleCache(this);\n\n this.worker = worker;\n this.identity = options.identity;\n this.paymentAddress = options.paymentAddress;\n this.options = options;\n this.maxWorkingSandboxes = options.maxWorkingSandboxes || 1;\n this.maxTotalSandboxes = this.maxWorkingSandboxes;\n\n // We're making the assumption that if a worker has at least 7 sandboxes, \n // then the worker has sufficient resources to handle 25% more sandboxes in memory.\n // This assumption may be overridden by changing workerSandboxThreshold.\n if (this.maxWorkingSandboxes >= this.workerSandboxThreshold)\n this.maxTotalSandboxes = Math.ceil(1.25 * this.maxWorkingSandboxes);\n // When # of sandboxes reaches this level, we more aggressively prune.\n this.mustPruneSandboxLevel = Math.ceil(1.5 * this.maxTotalSandboxes);\n // Last prune time stamp.\n this.lastPrune = 0;\n\n // Startup perf timer -- SAVE\n //this.lastTime = Date.now();\n\n // Supervisor may get created by Worker where options.cores or options.targetLoad is not defined.\n this.numCPU = this.maxWorkingSandboxes;\n this.numGPU = 1;\n this.portionToUseCPU = pct(100);\n this.portionToUseGPU = pct(100);\n\n if (options.cores)\n {\n this.numCPU = options.cores.cpu || this.numCPU;\n this.numGPU = options.cores.gpu || this.numGPU;\n }\n if (options.targetLoad)\n {\n this.portionToUseCPU = options.targetLoad.cpu || pct(100);\n this.portionToUseGPU = options.targetLoad.gpu || pct(100);\n }\n\n this.tuning = {\n maxCPUAlloc: this.portionToUseCPU, /**< Maximum amount of CPU time to attempt to use */\n maxGPUAlloc: this.portionToUseGPU, /**< Maximum amount of GPU time to attempt to use */\n watchdogInterval: 7, /**< How frequently to kick off an unsolicited requestTask */\n prefetchInterval: 20, /**< How many seconds into the future are looking to project capacity during work fetch */\n minSandboxStartDelay: 0.1, /**< seconds - base minimum of this.delayMs, scaled by this.delayScaler */\n maxSandboxStartDelay: 0.7, /**< seconds - base maximum random component of this.delayMs, scaled by this.delayScaler */\n };\n this.tuning = leafMerge(this.tuning, workerTuning.dcp.tuning);\n if (options.watchdogInterval > 0)\n this.tuning.watchdogInterval = options.watchdogInterval; // Override.\n //this.tuning.watchdogInterval = 0.25;\n\n /**\n * Fine tune this.delayMs.\n * Note: Please discuss any change with Paul, paul@kingsds.network.\n * XXXpfr @todo Finalize the delay tuning.\n */\n this.delayScaler = 0.5;\n\n debugging('supervisor') && console.debug('Supervisor.tuning', this.tuning);\n\n /**\n * Note: targetLoad is not properly implemented yet.\n * XXXpfr @todo Collaborate with Wes to get it right.\n * @type {Load}\n */\n this.targetLoad = new Load({\n cpu: Math.min(this.maxWorkingSandboxes, this.numCPU),\n gpu: Math.min(this.maxWorkingSandboxes, this.numGPU)\n }).scale(this.tuning.maxCPUAlloc, this.tuning.maxGPUAlloc);\n\n /** @type {string[]} */\n this.allowedOrigins = workerTuning.allowOrigins.any;\n /** @type {string[]} */\n this.fetchWorkFunctions = workerTuning.allowOrigins.fetchWorkFunctions;\n /** @type {string[]} */\n this.fetchArguments = workerTuning.allowOrigins.fetchArguments;\n /** @type {string[]} */\n this.fetchData = workerTuning.allowOrigins.fetchData;\n /** @type {string[]} */\n this.sendResults = workerTuning.allowOrigins.sendResults;\n\n // In localExec, do not allow work function or arguments to come from the 'any' origins\n if (this.options.localExec)\n {\n this.allowedOrigins = this.allowedOrigins.concat(options.allowedOrigins.any);\n this.fetchWorkFunctions = this.fetchWorkFunctions.concat(options.allowedOrigins.fetchWorkFunctions);\n this.fetchArguments = this.fetchArguments.concat(options.allowedOrigins.fetchArguments);\n this.fetchData = this.fetchData.concat(options.allowedOrigins.fetchData);\n this.sendResults = this.sendResults.concat(options.allowedOrigins.sendResults)\n }\n\n if (options.allowedOrigins && options.allowedOrigins.length > 0)\n this.allowedOrigins = options.allowedOrigins.concat(this.allowedOrigins);\n \n //\n // The following 3 configs are not in dcpConfig or worker configs (yet), but may be specified in local worker configs to override the defaults.\n //\n /** @type {number} - Maxiumum time interval where we check to prune used sandboxes. */\n this.pruneFrequency = workerTuning.pruneFrequency || 15 * 1000;\n /** @type {number} - When maxWorkingSandboxes >= workerSandboxThreshold, we allow an extra 25% of assigned sandboxes that won't be pruned. */\n this.workerSandboxThreshold = workerTuning.workerSandboxThreshold || 7;\n /** @type {number} - Prune the unused job managers >= cachedJobsThreshold. */\n this.cachedJobsThreshold = workerTuning.cachedJobsThreshold || 12;\n\n /** @type {Object.<Address, JobManager>} */\n this.jobMap = {}; \n /** @type {Sandbox[]} - All sandboxes that are being used by the job managers. Makes sure we don't lose sandboxes. */\n this.sandboxInventory = [];\n /** @type {Sandbox[]} - Started sandboxes that are not in sandboxInventory yet. */\n this.readiedSandboxes = [];\n /** @type {JobManager[]} */\n this.jobManagerInventory = new Inventory('jobManagers');\n /** @type {Synchronizer} */\n this.state = new Synchronizer('initial', [ 'initial', 'ready', 'reconnecting', 'stopping', 'stopped', 'broken']);\n\n /** @type {string} */\n this.lastDcpsid = undefined;\n /** @type {Connection} */\n this.taskDistributor = null;\n /** @type {Connection} */\n this.resultSubmitter = null;\n /** @type {Connection} */\n this.eventRouter = null;\n /** @type {Connection} */\n this.packageManager = null;\n /** @type {Array<object>} */\n this.resultSubmitterMessageQueue = [];\n /** @type {Array<object>} */\n this.eventRouterMessageQueue = [];\n /** @type {Array<object>} */\n this.packageManagerMessageQueue = [];\n\n /** @type {object} */\n this.schedulerConfig = leafMerge(dcpConfig.scheduler, options.schedulerConfig);\n\n /** @type {opaqueId} */\n this.workerId = localStorage.getItem('workerId');\n if (!this.workerId || this.workerId.length !== constants.workerIdLength)\n {\n this.workerId = generateOpaqueId();\n localStorage.setItem('workerId', this.workerId);\n }\n /** @type {object[]} */\n this.rejectedJobs = [];\n /** \n * An N-slot ring buffer of job addresses. Stores all jobs that have had no more than 1 slice run in the ring buffer.\n * Required for the implementation of discrete jobs \n * @type {RingBuffer} \n */\n this.ringBufferofJobs = new RingBuffer(100); // N = 100 should be more than enough. \n /** @type {boolean} - pseudo-mutex guarding requestTask. */\n this.isFetchingNewWork = false;\n\n // Start up the connections.\n this.instantiateAllConnections();\n\n /**\n * Note: DCP-3241 asks to test Android to see if we need this restriction any longer.\n * XXXpfr @todo Hopefully we can delete this @hack.\n */\n // @hack - dcp-env.isBrowserPlatform is not set unless the platform is _explicitly_ set,\n // using the default detected platform doesn't set it.\n // Fixing that causes an error in the wallet module's startup on web platform, which I\n // probably can't fix in a reasonable time this morning.\n // ~ER2020-02-20\n if (!options.maxWorkingSandboxes\n && DCP_ENV.browserPlatformList.includes(DCP_ENV.platform)\n && navigator.hardwareConcurrency > 1) {\n this.maxWorkingSandboxes = navigator.hardwareConcurrency - 1;\n if (typeof navigator.userAgent === 'string') {\n if (/(Android).*(Chrome|Chromium)/.exec(navigator.userAgent)) {\n this.maxWorkingSandboxes = 1;\n console.log('Doing work with Chromimum browsers on Android is currently limited to one sandbox');\n }\n }\n }\n}\nexports.Supervisor = Supervisor;\nSupervisor.prototype = Object.getPrototypeOf(new EventEmitter('Supervisor')); // Fake out VSCode -- get's rid of a billion red-squigglies.\nSupervisor.prototype = new EventEmitter('Supervisor');\n/**\n * Preserve the constructor property.\n * @constructor\n */\nSupervisor.prototype.constructor = Supervisor;\n\n/**\n * Set up sandboxes and interval timers, then start to search for work.\n * @param {boolean} [searchForWork=true] - When true, start searching for work.\n **/\nSupervisor.prototype.startWork = function Supervisor$startWork (searchForWork = true)\n{\n /* Provide opportunity for calling code to hook ready/error events. */\n setImmediate(async () => {\n try\n {\n if (this.state.isNot('initial'))\n {\n if (this.state.setIf('stopped', 'initial')) {}\n else if (this.state.setIf('reconnecting', 'initial')) {}\n else if (this.state.setIf('broken', 'initial')) {}\n else if (this.state.is('ready')) return\n else throw new Error(`Supervisor startWork is in unexpected state ${this.state}, aborting...`);\n }\n this.instantiateAllConnections();\n\n await this.createAndSaveSandboxes(this.maxWorkingSandboxes)\n .then(() => this.checkCapabilities());\n\n //console.log('startWork:1', Date.now() - this.lastTime); // SAVE\n\n // Beacon interval timer.\n this.progressReportTimer = setInterval(() => this.emitProgressReport(), (workerTuning.sandbox.progressReportInterval || 2 * 60 * 100));\n // Watchdog: requestTask-driven interval timer.\n this.watchdogTimer = setInterval(() => this.requestTask() , ms(this.tuning.watchdogInterval));\n if (DCP_ENV.platform === 'nodejs' && this.options.localExec)\n {\n /* Interval timer helps keep worker alive forever, which we don't want in localExec. */\n this.progressReportTimer.unref();\n this.watchdogTimer.unref();\n }\n\n this.state.set('initial', 'ready');\n\n //console.log('startWork:2', Date.now() - this.lastTime); // SAVE\n\n if (searchForWork)\n setImmediate(() => this.requestTask()); // Don't wait for watchdog.\n }\n catch(error)\n {\n this.state.set('initial', 'broken');\n this.emit('error', error);\n }\n });\n}\n\n/** Construct capabilities when necessary. */\nSupervisor.prototype.checkCapabilities = function Supervisor$checkCapabilities ()\n{\n if (!this.capabilities)\n {\n /**\n * Assign the capabilities of one the sandboxes before fetching slices from the scheduler.\n * @todo Remove this once fetchTask uses the capabilities of every sandbox to fetch slices.\n */\n const sandbox = this.readiedSandboxes.length > 0 ? this.readiedSandboxes[0] : this.sandboxInventory[0];\n if (sandbox)\n {\n this.capabilities = sandbox.capabilities;\n this.emit('capabilitiesCalculated', this.capabilities);\n }\n }\n\n if (DCP_ENV.isBrowserPlatform && this.capabilities.browser)\n this.capabilities.browser.chrome = DCP_ENV.isBrowserChrome;\n}\n\n// _Idx\n//\n// Important property-like functions.\n//\n\n/**\n * Universal delay milliseconds..\n * @returns {number}\n */\nSupervisor.prototype.delayMs = function Supervisor$delayMs (max = this.tuning.maxSandboxStartDelay, min = this.tuning.minSandboxStartDelay)\n{\n // Note: Please discuss any change with Paul, paul@kingsds.network.\n return 1000 * timeDilation * this.delayScaler * (min + Math.random() * (max - min));\n}\n\n/**\n * Indicates whether supervisor is ready for business.\n * @returns {boolean} - When true, the party is on...\n */\nSupervisor.prototype.isReady = function Supervisor$isReady()\n{\n return this.worker.working && this.state.is('ready');\n}\n\n/**\n * Safe access to Connection.close(...).\n * @param {string} name\n * @param {Connection} connection\n * @param {string} [reason='requested']\n * @param {boolean} [immediate=false]\n * @returns {string}\n */\nfunction safeCloseEx(name, connection, reason = 'requested', immediate = false)\n{\n if (connection)\n {\n let kind;\n if (connection.state.is('closed')) kind = 'closed';\n else if (connection.state.is('closing')) kind = 'closing';\n else if (connection.state.is('close-wait')) kind = 'close-wait';\n if (kind)\n {\n const msg = `${name} is ${kind}`;\n debuggingWarn && console.warn(`${msg}, do not try to close again.`);\n return msg;\n }\n else\n {\n debuggingWarn && console.warn(`${name} is being closed now.`);\n connection.close(reason, immediate);\n return null;\n }\n }\n return `${name} is null`;\n}\n\n/**\n * Safe access to Connection.close(...).\n * @param {Connection} connection\n * @param {string} [reason='requested']\n * @param {boolean} [immediate=false]\n * @returns {string}\n */\nfunction safeClose(connection, reason = 'requested', immediate = false)\n{\n return safeCloseEx('Connection', connection, reason, immediate);\n}\n\n/**\n * When true, the sandbox complete handler will look for another slice in the same job,\n * and if not found, then proceed to Supervisor.requestTask.\n * @returns {boolean}\n */\nSupervisor.prototype.runSliceFromSameJob = function Supervisor$runSliceFromSameJob()\n{\n //\n // Experimental, should be off by default.\n // Cf. sandbox complete handler in JobManager.hookUpSandboxListeners.\n //\n const disable = true;\n const tooManyJobs = this.activeJobCount() > this.maxWorkingSandboxes;\n return !disable && !tooManyJobs && this.unusedSandboxSlots() < 2;\n}\n\n/**\n * This function is used as the target number of sandboxes to be associated with slices and start working.\n * Warning: Do not rely on this information being 100% accurate -- it may change in the next instant.\n * @returns {number}\n */\nSupervisor.prototype.unusedSandboxSlots = function Supervisor$unusedSandboxSlots ()\n{\n return this.maxWorkingSandboxes - this.workingSliceCount();\n}\n\n/**\n * Create errorObj with error.code if it exists.\n * @param {Error} error\n * @returns {object}\n */\nSupervisor.prototype.checkCode = function Supervisor$checkCode (error)\n{\n if (!error) return '';\n const errorObj = { message: error.message };\n if (error['errorCode']) errorObj['errorCode'] = error['errorCode'];\n if (error['code']) errorObj['code'] = error['code'];\n return errorObj;\n}\n\n/**\n * Remove stack trace from error.\n * May not work when error is a string with no new-lines.\n * @param {string|Error} error\n * @returns {string|Error}\n */\nSupervisor.prototype.removeStackTrace = function Supervisor$removeStackTrace (error)\n{\n if (typeof error === 'string')\n {\n const errorLines = error.split('\\n');\n return (errorLines && errorLines.length > 0) ? errorLines[0] : error;\n }\n if (error instanceof Error)\n return error.message;\n return error;\n}\n\n// _Idx\n//\n// Dtors: screenSaverDestroy, stopWork, purgeJob.\n//\n\n/**\n * If we cannot create a new sandbox, that probably means we're on a screensaver worker\n * and the screensaver is down. So return the slices to the scheduler.\n */\nSupervisor.prototype.screenSaverDestroy = function Supervisor$screenSaverDestroy()\n{\n debugging('supervisor') && console.debug(`Supervisor.screenSaverDestroy: destroying all job managers and terminating all sandboxes.`);\n this.jobManagerInventory.forEach(jm => jm.destroy());\n this.jobManagerInventory = new Inventory('jobManagers');\n\n this.readiedSandboxes.forEach(sandbox => {\n if (!sandbox.isTerminated) sandbox.terminate(false);\n });\n this.readiedSandboxes = [];\n\n this.sandboxInventory.forEach(sandbox => {\n if (!sandbox.isTerminated) sandbox.terminate(false);\n });\n this.sandboxInventory = [];\n}\n\n/**\n * Terminates sandboxes and returns slices.\n * Sets the working flag to false, call @this.work to start working again.\n * \n * If forceTerminate is true: Terminates all sandboxes and returns all slices.\n * If forceTerminate is false: Terminates non-working sandboxes and returns initial and ready slices.\n *\n * @param {boolean} [forceTerminate = true] - true if you want to stop the sandboxes from completing their current slice.\n * @returns {Promise<void>}\n */\nSupervisor.prototype.stopWork = async function Supervisor$stopWork (forceTerminate = true)\n{\n selectiveDebugging && console.debug(`Supervisor.stopWork(${forceTerminate},${this.state}): terminating sandboxes and returning slices to scheduler.`);\n\n // Do a hard flush of the microtask queue and finish the current event loop.\n await new Promise((resolve) => setImmediate(() => setTimeout(resolve, 0)));\n\n if (this.state.setIf('ready', 'stopping')) {}\n else if (this.state.setIf('reconnecting', 'stopping')) {}\n else if (this.state.setIf('broken', 'stopping')) {}\n else if (this.state.is('stopped')) return\n else throw new Error(`Supervisor stopWork is in unexpected state ${this.state}, aborting...`);\n\n this.instantiateAllConnections();\n\n this.readiedSandboxes.forEach((sandbox) => {\n if (!sandbox.isTerminated) sandbox.terminate(false);\n });\n this.readiedSandboxes = [];\n\n if (forceTerminate)\n {\n for (const jm of this.jobManagerInventory)\n jm.destroy();\n\n this.sandboxInventory.forEach((sandbox) => {\n if (!sandbox.isTerminated) sandbox.terminate(false);\n });\n }\n else\n {\n let activeSliceCount = 0;\n const slicesToReturn = [];\n for (const jm of this.jobManagerInventory)\n {\n //jm.dumpSlices(`stopWork1:${jm.address}`);\n const queuedSlices = jm.queuedSlices;\n slicesToReturn.push(...queuedSlices);\n jm.removeSlices(queuedSlices);\n activeSliceCount += jm.activeSlices.length;\n jm.nonWorkingSandboxes.forEach((sandbox) => jm.returnSandbox(sandbox));\n }\n\n const reason = `Supervisor stopWork(${forceTerminate}): returning all non-finished slices that are not working.`;\n this.returnSlices(slicesToReturn, reason, false /*removeSlices*/);\n\n await new Promise((resolve) => {\n if (activeSliceCount === 0)\n resolve();\n // Resolve and finish work once all sandboxes have finished submitting their results.\n this.on('submitFinished', () => {\n if (--activeSliceCount === 0)\n {\n debuggingWarn && console.warn('All sandboxes empty, stopping worker and closing all connections');\n resolve();\n }\n });\n });\n\n for (const jm of this.jobManagerInventory)\n {\n //jm.dumpSlices(`stopWork2:${jm.address}`);\n jm.safeSandboxes.forEach((sandbox) => sandbox.terminate(false));\n jm._sandboxInventory = [];\n }\n }\n this.sandboxInventory = [];\n this.jobManagerInventory = new Inventory('jobManagers');\n\n this.closeConnections();\n\n this.emit('stop');\n this.state.set('stopping', 'stopped');\n}\n\n/**\n* Purge all traces of the job.\n* @param {JobManager} jobManager \n*/\nSupervisor.prototype.purgeJob = function Supervisor$purgeJob (jobManager)\n{\n selectiveDebugging && console.debug(`Supervisor.purgeJob ${jobManager.identifier}.`);\n this.jobManagerInventory.delete(jobManager);\n this.cache.removeJob(jobManager.address);\n jobManager.destroy();\n}\n\n// _Idx\n//\n// Connection code.\n//\n\n/** \n * Connect the supervisor to a given scheduler sub-service. Reconnection \"DDoS\" from workers\n * mitigated via an exponential backoff algorithm.\n *\n * DCPv4 connections are lazily-initiated. Successful connection establishment detected by\n * observing the payload event, which normally triggers during versioning.\n */\nSupervisor.prototype.connectTo = function Supervisor$connectTo(which)\n{\n const config = (which === 'packageManager') ? dcpConfig.packageManager : this.schedulerConfig.services[which];\n const retryMinSleepMs = 100;\n const payloadResetCount = 3; /* How many payloadCount before we reset retryNextSleepMs. */\n \n var retryNextSleepMs = retryMinSleepMs;\n var payloadCount = 0;\n var options = leafMerge(/* ordered from most to least specific */\n workerTuning.dcp.connectionOptions.default,\n workerTuning.dcp.connectionOptions[which],\n workerTuning.dcp.connectionOptions[config.location.href],\n );\n\n /**\n * The payload event handler is an interesting way to handle exponential backoff\n * for the delay in this.reconnect.\n * XXXpfr @todo Revisit exponential backoff for this.reconnect in Sup2 Part II.\n */\n\n const handlePayloadEventFn = (ev) =>\n {\n if (++payloadCount === payloadResetCount)\n {\n this[which].removeEventListener('payload', handlePayloadEventFn);\n retryNextSleepMs = retryMinSleepMs; \n }\n }\n\n this[which] = new dcp4.Connection(config, this.identity, options);\n\n debugging() && console.debug(`Opening new connection ${this[which].id} to ${which}.`)\n\n const messageQueue = this[which + 'MessageQueue'];\n if (messageQueue && messageQueue.length > 0)\n resendRejectedMessages(this[which], messageQueue);\n\n this[which].on('payload', handlePayloadEventFn);\n\n this['reconnect' + which] = this.reconnect.bind(this, which);\n this[which].on('close', () => { this['reconnect' + which](); });\n}\n\n/**\n * Reconnect logic.\n * @param {string} which -- Name of the connection \n */\nSupervisor.prototype.reconnect = async function Supervisor$reconnect(which)\n{\n debugging('supervisor') && console.debug(`Supervisor.reconnect: ${which}:`, this.state.valueOf(), this[which].state.valueOf());\n const stateChange = this.state.testAndSet('ready', 'reconnecting');\n if (stateChange || this.state.is('reconnecting'))\n {\n await a$sleepMs(100); // Sleep for 100ms , maybe exp-backoff later if we need it.\n this.connectTo(which);\n if (stateChange) this.state.set('reconnecting', 'ready');\n debugging() && console.debug(`Supervisor.reconnect: Trying to reconnect ${which}`, this.state.valueOf(), this[which].state.valueOf());\n }\n}\n\n/**\n * Close a connection properly.\n * @param {string} which -- Name of the connection to close.\n */\nSupervisor.prototype.closeConnection = function Supervisor$closeConnection(which)\n{\n if (this[which])\n {\n this[which].off('close', this['reconnect' + which]);\n this[which].close();\n this[which] = null;\n }\n}\n\n/**\n * Close all connections.\n */\nSupervisor.prototype.closeConnections = function Supervisor$closeConnections()\n{\n /** XXXpfr @todo Remove when bug DCP-3094 is fixed. */\n a$sleepMs(500);\n\n this.closeConnection('taskDistributor');\n this.closeConnection('resultSubmitter');\n this.closeConnection('eventRouter');\n this.closeConnection('packageManager');\n}\n\n/**\n * Broadcast keepalive to all connections and optionally start up sandboxes.\n * @param {boolean} [createSandbox=false] - When true, creates sandboxes for unused sandbox slots.\n */\nSupervisor.prototype.keepalive = function Supervisor$keepalive(createSandbox = false)\n{\n return Promise.all([\n this.taskDistributor.keepalive(),\n this.resultSubmitter.keepalive(),\n this.eventRouter .keepalive(),\n this.packageManager .keepalive(),\n (createSandbox ? this.createAndSaveSandboxes(this.maxWorkingSandboxes) : Promise.resolve()),\n ]);\n}\n\n/**\n * Open all connections. Used when supervisor is instantiated or stopped/started to initially open connections.\n */\nSupervisor.prototype.instantiateAllConnections = function Supervisor$instantiateAllConnections ()\n{\n if (!this.taskDistributor)\n this.connectTo('taskDistributor');\n if (!this.eventRouter)\n this.connectTo('eventRouter');\n if (!this.resultSubmitter)\n this.connectTo('resultSubmitter');\n if (!this.packageManager)\n this.connectTo('packageManager');\n}\n\n/**\n * Try sending messages that were rejected on an old instance of the given connection.\n * @param {Connection} connection\n * @param {Array<object>} messageQueue\n */\nasync function resendRejectedMessages (connection, messageQueue)\n{\n var message = messageQueue.shift();\n do \n {\n try\n {\n await connection.send(message.operation, message.data);\n }\n catch (error)\n {\n debuggingError && console.error(`Failed to resend message ${message.operation} to scheduler: ${error}. Will try again on a new connection.`);\n messageQueue.unshift(message);\n safeClose(connection);\n break;\n }\n message = messageQueue.shift();\n } while (message);\n}\n \n// _Idx\n//\n// Work: Distribute slice to sandboxes.\n//\n\n/** \n * UNUSED\n * @deprecated\n * Round-robin through the job managers, picking 1 slice to run each time.\n * Try to have the same number of working sandboxes for each job.\n * Try to run a slice on every available sandbox.\n */\nSupervisor.prototype.workOnCurrentTask = function Supervisor$workOnCurrentTask ()\n{\n return this.roundRobinSlices();\n}\n\n/**\n * This is for compatibility with Supervisor1 in sa worker.\n * When we get rid of Supervisor1 we can delete the ref in sa worker and then get rid of this function.\n */\nSupervisor.prototype.setDefaultIdentityKeystore = function Supervisor$setDefaultIdentityKeystore ()\n{\n}\n\n/**\n * Called in Worker.start().\n * Initial entry point after Worker constructor.\n * We need to start searching for work here to allow starting and stopping a worker.\n */\nSupervisor.prototype.work = function Supervisor$work ()\n{\n // Set up sandboxes and interval timers, then start to search for work.\n this.startWork();\n}\n\n/** \n * Round-robin through the job managers, picking 1 slice to run each time.\n * Try to have the same number of working sandboxes for each job.\n * Try to run a slice on every available sandbox.\n */\nSupervisor.prototype.roundRobinSlices2 = function Supervisor$roundRobinSlices2 ()\n{\n try\n {\n let first = true;\n const cursor = this.makeJobSelectionCursor(this.jobManagerInventory);\n while (true)\n {\n const slice = cursor.next();\n if (!slice) break; /* No more work can fit. */\n debugging('supervisor') && console.debug('roundRobinSlices: Executing slice', slice.identifier);\n slice.markAsReserved();\n slice.jobManager.runSlice(slice, first ? 0 : this.delayMs());\n first = false;\n }\n }\n finally\n {\n this.isFetchingNewWork = false;\n }\n}\n\n/**\n * We try to balance so that each job has the same number of working slices.\n *\n * NOTES:\n * 1) If count is such that it cannot be distributed evenly, we do not yet randomly assign the extras.\n * E.g. 3 jobs, j1, j2, j3: count = 5 -- so 2 jobs get extras -- the extras need to be assigned randomly.\n * @todo Assign the extras randomly.\n * 2) this.roundRobinSlices is not going to be what we use when sup2 is in final form.\n * We want to this.makeJobSelectionCursor and something like this.roundRobinSlices2\n *\n * In the outer loop,\n * when numworkingSandboxes=1, try to get a slice running for each job with 0 working sandboxes.\n * when numworkingSandboxes=2, try to get a slice running for each job with 1 working sandboxes.\n * when numworkingSandboxes=3, try to get a slice running for each job with 2 working sandboxes. Etc.\n * The idea is to balance the number of slices working on each job.\n * @param {number} [count=0] - The number of new slices to try to get running in sandboxes.\n */\nSupervisor.prototype.roundRobinSlices = function Supervisor$roundRobinSlices (count = 0)\n{\n try\n {\n if (!count) count = this.unusedSandboxSlots();\n if (!count || this.readySliceCount() < 1)\n return Promise.resolve();\n\n const slices = [];\n let numScheduled = 0\n let readyJobManagers = this.jobManagerInventory.filter(jm => jm.ready);\n let first = true;\n selectiveDebugging && console.debug('roundRobinSlices: START count', count, 'numJobMgrs', readyJobManagers.length, 'totalWorking(w/r/wo/wsbx/sbx)', this.workingSliceCount(), this.reservedSliceCount(), this.workingSliceOnlyCount(), this.workingSandboxCount(), this.sandboxCount());\n\n for (let numWorkingSandboxes = 1; numWorkingSandboxes <= this.maxWorkingSandboxes; numWorkingSandboxes++)\n {\n let sliceCount = 0;\n const beginNumScheduled = numScheduled;\n for (const jobMan of readyJobManagers)\n {\n const _readySlices = jobMan.readySlices;\n sliceCount += _readySlices.length\n const skip = numWorkingSandboxes <= jobMan.workingSlices.length;\n\n if (skip || _readySlices.length < 1)\n {\n // Noisy log message turned off by default.\n //debugging('supervisor') && console.debug('RRS0(numS, beginNumS, count, sliceCount, skip, _ready, numWorkingS(loop), workingSlices):', numScheduled, beginNumScheduled, count, sliceCount, skip, _readySlices.length, numWorkingSandboxes, jobMan.workingSlices.length);\n continue;\n }\n\n const slice = _readySlices[0];\n slices.push(slice);\n\n slice.markAsReserved();\n jobMan.runSlice(slice, first ? 0 : this.delayMs());\n\n first = false;\n if (++numScheduled >= count)\n break;\n }\n if (numScheduled >= count)\n {\n debugging('supervisor') && console.debug('RRS1(numS, beginNumS, count, sliceCount):', numScheduled, beginNumScheduled, count, sliceCount);\n break;\n }\n if (beginNumScheduled === numScheduled && sliceCount < 1)\n {\n debugging('supervisor') && console.debug('RRS2(numS, beginNumS, count, sliceCount):', numScheduled, beginNumScheduled, count, sliceCount);\n break;\n }\n }\n if (selectiveDebugging)\n {\n console.debug(`roundRobinSlices(working:(w/r/wo/wsbx/sbx)${this.workingSliceCount()},${this.reservedSliceCount()},${this.workingSliceOnlyCount()},${this.workingSandboxCount()},${this.sandboxCount()}): Started ${slices.length}/${numScheduled}/${count} scheduled slices`, compressSlices(slices));\n const sliceGrouper = {};\n slices.forEach((slice) => {\n if (!sliceGrouper[slice.jobAddress]) sliceGrouper[slice.jobAddress] = { cnt: 1, working: slice.jobManager.workingSlices.length, queued: slice.jobManager.queuedSlices.length, assigned: slice.jobManager.assignedSandboxes.length, estimation: slice.jobManager.isEstimation };\n else sliceGrouper[slice.jobAddress].cnt++;\n });\n console.debug(sliceGrouper);\n }\n if (selectiveDebugging2)\n {\n const jobGrouper = {};\n this.jobManagerInventory.forEach((jm) => {\n jobGrouper[jm.address] = { working: jm.workingSlices.length, queued: jm.queuedSlices.length, assigned: jm.assignedSandboxes.length, estimation: jm.isEstimation };\n });\n console.debug(jobGrouper);\n }\n }\n finally\n {\n this.isFetchingNewWork = false;\n }\n}\n\n/**\n * @callback cbNext\n * @returns {Slice}\n */\n\n/** \n * Factory function which instantiates a JobSelectionCursor. A JobSelectionCursor\n * steps the order that job slices should be selected for execution in the supervisor,\n * given the current state of the supervisor and the availability of jobs when the\n * inventory was snapshot. The entire slice scheduling algorithm is represented by\n * this cursor.\n *\n * The basic idea behind the scheduling of slices in this implementation is to keep as\n * many slices from different jobs running as possible, so as to reduce the likelihood\n * of resource contention between sandboxes.\n *\n * Slices are scheduled in here based on the following ruleset:\n * - pick a slice from the longest job that isn't running\n * - choose a slice from the remaining jobs, in order for shortest to longest slice time\n * - if there are any jobs which are nearly finished, every other slice comes from one\n * of these jobs, selected randomly????? <-- NYI. XXXpfr @todo Think about implementing...\n * - jobs which have slicePriority set by the task-distributor may be chosen in place\n * of slices in the above algorith. Jobs with a slicePriority closer to 1 are more likely\n * to exhibit this behaviour.\n * @param {JobManager[]} jobManagerInventory\n * @returns { { next: cbNext } }\n */\nSupervisor.prototype.makeJobSelectionCursor = function Supervisor$JobSelectionCursor (jobManagerInventory)\n{\n /* Variables in this scope function as state information for next() */\n var jobManIdx = 0;\n /** @type {JobManager[]} - All job managers that are ready that have at least one ready slice. */\n var jobManList;\n /** @type {JobManager[]} - All jobManList elements that correspond to preferred jobs. */\n var prefJobManList;\n /* Number of working sandboxes allowed for a given job. */\n var concurrency = 1;\n const that = this;\n \n function seed (_concurrency)\n {\n function countSandboxes(jobAddress)\n {\n const jobManager = that.jobMap[jobAddress];\n if (!jobManager) throw new Error(`Lost track of job manager for address '${jobAddress}'`);\n return jobManager.workingSlices.length;\n }\n \n // Reset.\n jobManIdx = 0;\n\n /* Populate jobManList with jobs which are ready and have at least one slice which is ready. */\n jobManList = jobManagerInventory.filter((jobMan) => jobMan.ready && jobMan.readySlices.length > 0);\n\n /* Populate jobManList with jobManagers whose # of working slices is less than _concurrency. */\n jobManList = jobManList.filter((jobMan) => countSandboxes(jobMan.address) < _concurrency);\n\n /* Increasing sort. */\n jobManList.sort((a,b) => a.estimateWallMs - b.estimateWallMs);\n\n /* Populate prefJobManList with jobs from jobManList which also have a slicePreference set. */\n prefJobManList = jobManList.filter(jobMan => jobMan.hasOwnProperty('slicePreference') );\n }\n\n /**\n * Takes slices off the ready list, marks it reserved and increments workingSandboxCoun,\n * because the slice will soon be working.\n * @param {JobManager} jobMan \n * @returns {Slice}\n */\n function reserveSlice (jobMan)\n {\n const _readySlices = jobMan.readySlices;\n if (_readySlices.length > 0)\n {\n const slice = _readySlices[0];\n slice.markAsReserved();\n return slice;\n }\n return null;\n }\n\n /**\n * Each invocation of next() identifies one slice to run, or returns false if none can run.\n * @returns {Slice}\n */\n function next ()\n {\n /* Adjust order to schedule the heaviest job's first slice asap. */\n jobManList.unshift(jobManList.pop());\n\n let workingSliceCount;\n do\n {\n seed(concurrency);\n\n /* Maybe schedule a prefered job slice based on random chance .*/\n if (prefJobManList.length > 0)\n {\n let prioRan = Math.random();\n let list = prefJobManList.filter(jm => jm['slicePreference'] >= prioRan);\n\n if (list.length > 0)\n {\n const jobMan = list[list.length * Math.random()];\n const slice = reserveSlice(jobMan);\n if (slice)\n return slice;\n }\n }\n\n /* Schedule a slice from next job; jobs are in increasing order of estimated run time. */\n while (jobManIdx < jobManList.length)\n {\n const jobMan = jobManList[jobManIdx];\n jobManIdx++;\n const slice = reserveSlice(jobMan);\n if (slice)\n return slice;\n }\n\n /* If this is reached, we did not schedule a slice with current seed. We need\n * to re-seed to look for newly-available work and sandboxes, ratcheting up the\n * concurrency (max # of each job running) until we find something we can do.\n */\n workingSliceCount = that.workingSliceCount();\n debugging() && console.debug(`job selection - no slice found from ${jobManList.length} jobs for concurrency=${concurrency} and ${workingSliceCount} working sandboxes`);\n } while (jobManList.length > 0 && workingSliceCount < that.maxWorkingSandboxes && concurrency++ < that.maxWorkingSandboxes);\n\n return false; /* Did not find any more work that fits. */\n }\n \n return { next };\n}\n\n/**\n * Handle sandbox.work(slice) errors.\n * @param {Sandbox} sandbox \n * @param {Slice} slice \n * @param {Error} error \n * @return {Promise<string>}\n */\nSupervisor.prototype.handleSandboxWorkError = async function Supervisor$handleSandboxWorkError (sandbox, slice, error)\n{\n if (!slice.isWorking) // Sanity. Exception should never fire.\n throw new Error(`handleSandboxWorkError: slice ${slice.identifier} must be WORKING.`);\n\n let logLevel, reason;\n \n if (error instanceof SandboxError)\n {\n logLevel = 'warn';\n reason = error.errorCode;\n // The message and stack properties of error objects are not enumerable,\n // so they have to be copied into a plain object this way\n // @ts-ignore\n error = Object.getOwnPropertyNames(error).reduce((o, p) => {\n o[p] = error[p]; return o;\n }, { message: 'Unexpected worker error' });\n }\n else\n {\n logLevel = 'error';\n if (error)\n reason = `Slice ${slice.sliceNumber} in state ${slice.state} of job ${slice.jobAddress} failed to complete execution with error ${this.checkCode(error)}.`;\n else\n {\n reason = `Slice ${slice.sliceNumber} in state ${slice.state} of job ${slice.jobAddress} failed to complete execution.`;\n error = new Error(reason);\n }\n // This error was unrelated to the work being done, so just return the slice\n // in the promise.catch in JobManager.runSliceOnSandbox .\n assert(slice.result === null);\n }\n \n this.handleFailedSlice(slice, error);\n\n let errorString;\n switch (reason)\n {\n case 'ENOPROGRESS':\n errorString = 'No progress error in sandbox.\\n';\n break;\n case 'ESLICETOOSLOW':\n errorString = 'Slice too slow error in sandbox.\\n';\n break;\n case 'EUNCAUGHT':\n errorString = `Uncaught error in sandbox ${error.message}.\\n`;\n break;\n case 'EFETCH':\n // The status.js processing does not have a case for 'EFETCH' yet.\n errorString = `Could not fetch data: ${error.message}.\\n`;\n break;\n }\n\n // Always display max info under debug builds, otherwise maximal error.\n // messages are displayed to the worker, only if both worker and client agree.\n const displayMaxInfo = slice.jobManager.displayMaxDiagInfo;\n\n const errorObject = {\n jobAddress: truncateAddress(slice.jobAddress, addressTruncationLength),\n sliceNumber: slice.sliceNumber,\n sandbox: sandbox.id,\n jobName: sandbox.public ? sandbox.public.name : 'unnamed',\n };\n if (displayMaxInfo)\n errorObject.stack += '\\n --------------------\\n' + (error.stack.split('\\n').slice(1).join('\\n'));\n\n if (error.name === 'EWORKREJECT')\n {\n reason = 'EWORKREJECT'; // The status.js processing does not have a case for 'EWORKREJECT' yet.\n error.stack = 'Sandbox was terminated by work.reject()';\n await this.handleWorkReject(sandbox, slice, error.message);\n }\n else\n {\n this.returnSlice(slice, reason);\n slice.jobManager.returnSandbox(sandbox);\n }\n\n if (errorString)\n console[logLevel](errorString, errorObject);\n else if (error.name === 'EWORKREJECT')\n console[logLevel](`Slice rejected work: ${error.message}`)\n else\n console[logLevel](`Slice failed: ${error.message}\\n`, errorObject);\n\n return reason;\n}\n\n/**\n * Slice has thrown error during execution:\n * Mark slice as failed, compensate when job is dicrete, emit events.\n * @param {Slice} slice\n * @param {Error} error\n */\nSupervisor.prototype.handleFailedSlice = function Supervisor$handleFailedSlice (slice, error)\n{\n assert(error, 'error must be valid');\n slice.collectResult(error, false);\n\n // If the slice from a job never completes and the job address exists in the ringBufferofJobs,\n // then we remove it to allow for another slice (from the same job) to be obtained by fetchTask\n this.ringBufferofJobs.buf = this.ringBufferofJobs.filter(element => element !== slice.jobAddress);\n\n this.emit('submitSliceFailed', error);\n this.emit('submitFinished');\n}\n\n// _Idx\n//\n// Return slices and sent progress reports to result-submitter-results.\n// Return slices to result-submitter-status which marks the\n// corresponding row in activeSlices to be rescheduled on usually another worker.\n//\n\n/**\n * Bulk-return multiple slices, possibly for assorted jobs.\n * Returns slices to the scheduler to be redistributed.\n * Called in the sandbox terminate handler and purgeAllWork(jobAddress)\n * and stopWork(forceTerminate).\n *\n * @param {Slice[]} slices - The slices to return to the scheduler.\n * @param {string} [reason='unknown'] - Optional reason for the return: 'ENOPROGRESS', 'EUNCAUGHT', 'ESLICETOOSLOW', 'unknown'.\n * @param {boolean} [removeSlices=true] - When true, removes slices from this.sliceInventory .\n * @returns {Promise<*>} - Response from the scheduler.\n */\nSupervisor.prototype.returnSlices = function Supervisor$$returnSlices (slices, reason = 'unknown', removeSlices = true)\n{\n if (!slices || !slices.length) return Promise.resolve();\n debugging('supervisor') && console.debug('Supervisor.returnSlices: Returning slices', slices.map(slice => slice.identifier));\n\n const slicePayload = constructReturnSliceBuckets(slices, reason);\n if (removeSlices) slices.forEach((slice) => slice.jobManager.removeSlice(slice));\n\n try\n {\n return this.resultSubmitter.send('status', {\n worker: this.workerId,\n slices: slicePayload,\n }).catch(error => {\n const errorInfo = slices.map(slice => slice.identifier).sort();\n debuggingError && console.error('Failed to return slice(s)', { errorInfo, error }, 'Will try again on new connection.');\n this.resultSubmitterMessageQueue.push({ operation: 'status', data: { worker: this.workerId, slices: slicePayload } })\n safeClose(this.resultSubmitter);\n });\n }\n catch (error)\n {\n /* resultSubmitter can be null if worker is stopped */\n debuggingError && console.error(`Failed to return slices ${compressSlices(slices)}, no connection to result submitter:`, error);\n }\n}\n\n/** XXXpfr @todo TEMP -- Remove when sup2 replaces sup1 */\nfunction compressSlices(sliceArray)\n{\n const jobSliceMap = toJobMap(sliceArray, slice => slice.sliceNumber);\n return compressJobMap(jobSliceMap, false /* skipFirst*/, addressTruncationLength);\n}\n\n/**\n * Takes a slice and returns it to the scheduler to be redistributed.\n * Usually called when an exception is thrown by sandbox.work(slice, startDelayMs) .\n * Or when the supervisor tells it to forcibly stop working.\n *\n * @param {Slice} slice - The slice to return to the scheduler.\n * @param {string} [reason] - Optional reason for the return: 'ENOPROGRESS', 'EUNCAUGHT', 'ESLICETOOSLOW', 'unknown'.\n * @returns {Promise<*>} - Response from the scheduler.\n */\nSupervisor.prototype.returnSlice = function Supervisor$$returnSlice (slice, reason)\n{\n assert(slice.sliceNumber > 0 && slice.jobManager);\n debugging() && console.debug(`Supervisor.returnSlice: Returning slice ${slice.identifier} with reason ${reason}.`);\n\n if (!this.resultSubmitter)\n this.connectTo('resultSubmitter');\n\n try\n {\n slice.jobManager.removeSlice(slice);\n const payload = slice.getReturnMessagePayload(this.workerId, reason);\n return this.resultSubmitter.send('status', payload)\n .catch(error => {\n debuggingError && console.error('Failed to return slice', {\n sliceNumber: slice.sliceNumber,\n jobAddress: slice.jobAddress,\n status: slice.state.valueOf(),\n error,\n }, 'Will try again on a new connection.');\n this.resultSubmitterMessageQueue.push({ operation: 'status', data: payload });\n safeClose(this.resultSubmitter);\n });\n }\n catch (error)\n {\n /* resultSubmitter can be null if worker is stopped */\n debuggingError && console.error(`Failed to return slice ${slice.identifier}, no connection to result submitter:`, error);\n }\n}\n\n/**\n * Send beacon to status.js for 'progress' and sliceStatus.scheduled.\n *\n * Run in an interval created in @constructor .\n * @returns {Promise<void|Response>}\n */\nSupervisor.prototype.emitProgressReport = function emitProgressReport () \n{\n const slices = constructSliceBuckets( this.readySlices(), sliceStatus.scheduled );\n constructSliceBuckets( this.workingSlices(), 'progress', slices );\n\n debugging('supervisor') && console.debug('emitProgressReport:', stringify(slices));\n\n if (slices.length)\n {\n const progressReportPayload = {\n worker: this.workerId,\n slices,\n };\n\n try\n {\n return this.resultSubmitter.send('status', progressReportPayload)\n .catch(error => {\n debuggingError && console.error('479: Failed to send status beacon update:', error/*.message*/);\n this.resultSubmitterMessageQueue.push({ operation: 'status', data: progressReportPayload })\n safeClose(this.resultSubmitter);\n });\n }\n catch (error) \n {\n /* resultSubmitter can be null if worker is stopped */\n debuggingError && console.error(`Failed to emit progress report, no connection to result submitter:`, error);\n }\n }\n}\n\n/**\n * Add a slice to the slice buckets being built. If a sliceBucket already exists for the\n * job-status-authMessage tuple, then the slice will be added to that, otherwise a new\n * sliceBucket will be added to the buckets.\n *\n * @param {Slice[]} slices - The slices.\n * @param {String} status - Status update, eg. progress or scheduled.\n * @param {Object[]} [sliceBuckets] - Slice buckets being built. Will be mutated in place.\n * @returns {Object[]} - mutated sliceBuckets array\n */\nfunction constructSliceBuckets (slices, status, sliceBuckets)\n{\n const jobMap = {};\n for (const slice of slices)\n {\n assert(slice.sliceNumber > 0 );\n if (!jobMap[slice.jobAddress]) jobMap[slice.jobAddress] = [];\n\n // Try to find a sliceBucket in the buckets which matches the job, status, and auth message.\n let sliceBucket = jobMap[slice.jobAddress].find(desc => {\n return desc.status === status\n && desc.authorizationMessage === slice.authorizationMessage;\n });\n\n if (!sliceBucket) jobMap[slice.jobAddress].push(slice.getMessage(status));\n else sliceBucket.sliceNumbers.push(slice.sliceNumber);\n }\n\n if (!sliceBuckets) return Object.values(jobMap);\n sliceBuckets.push(...Object.values(jobMap));\n return sliceBuckets;\n}\n \n/**\n * Add a slice to the returnSlice bucket being built. If a sliceBucket already exists for the\n * job-isEstimation-authMessage-reason tuple, then the slice will be added to that, otherwise a new\n * sliceBucket will be added to the buckets.\n *\n * @param {Slice[]} slices - The slices.\n * @param {String} [reason] - Optional reason to further characterize status; e.g. 'ENOPROGRESS', 'EUNCAUGHT', 'ESLICETOOSLOW', 'unknown'.\n * @param {Object[]} [sliceBuckets] - Optional slice buckets being built. Will be mutated in place.\n * @returns {Object[]} - mutated sliceBuckets array\n */\nfunction constructReturnSliceBuckets (slices, reason, sliceBuckets)\n{\n const jobMap = {};\n for (const slice of slices)\n {\n assert(slice.sliceNumber > 0 );\n if (!jobMap[slice.jobAddress]) jobMap[slice.jobAddress] = [];\n \n // Try to find a sliceBucket in the buckets which matches the job, estimation status, reason, and auth message.\n let sliceBucket = jobMap[slice.jobAddress].find(desc => {\n return desc.isEstimationSlice === slice.isEstimation\n && desc.authorizationMessage === slice.authorizationMessage\n && desc.reason === reason;\n });\n\n if (!sliceBucket) \n jobMap[slice.jobAddress].push(slice.getMessage('return', { isEstimationSlice: slice.isEstimation, reason }));\n else sliceBucket.sliceNumbers.push(slice.sliceNumber);\n }\n\n if (!sliceBuckets) return Object.values(jobMap);\n sliceBuckets.push(...Object.values(jobMap));\n return sliceBuckets;\n}\n \n// _Idx\n//\n// Task Distributor (TD): requestTask (Rq) support -- communication with TD.\n//\n\n/**\n * XXXpfr @todo Needs Work\n * For a given job, the scheduler stores an EMA approximation of average slice completion time in\n * jobPerfData.sliceCPUTime (and jobPerfData.sliceGPUTime, but we don't do the GPU analysis yet.)\n * However, each worker also tracks the same information and the ratio of local-info to scheduler-info\n * is returned by this.conversionQuantum so we can tell the task distributor how much work to return\n * from requestTask so that the work actually takes 5 minutes to complete when using all the worker sandboxes.\n * Note: \n * We average the completion times over the current jobs.\n * Define completion time in terms of sliceC(G)PUTime and sliceC(G)PUDensity\n * completion-time = (sliceCGPUTime + sliceCGPUTime) / ( sliceCPUDensity + sliceGPUDensity);\n * The local completion time is an EMA approximation of local completion-time as computed by Supervisor.recordResult.\n * The scheduler completion-time is computed directly from the corresponding row in jobPerfData.\n */\nSupervisor.prototype.conversionQuantum = function Supervisor$conversionQuantum()\n{\n let globalSpeed = 0, localSpeed = 0;\n for (const jobMan of this.jobManagerInventory)\n {\n const _globalTime = jobMan.globalTime;\n const _localTime = jobMan.statistics.ema;\n if (_globalTime > 0 && _localTime > 0)\n {\n globalSpeed += _globalTime;\n localSpeed += _localTime;\n }\n }\n const conversion = globalSpeed > 0 ? localSpeed / globalSpeed : 1;\n return Math.min(Math.max(conversion, 0.2), 5.0); // Truncate if conversion is too bizarre.\n}\n\n/**\n * Remove all unreferenced jobs in this.jobManagerInventory and this.cache.\n * Since job-managers are inserted into this.jobManagerInventory with a push, the job managers at the beginning are oldest.\n * Only delete #deleteCount of the oldest job-managers:\n * let deleteCount = this.jobManagerInventory.length - cachedJobsThreshold;\n * Edit cachedJobsThreshold to adjust the cache cleanup threshold.\n * @param {object[]} [newJobKeys=[]] - Jobs that should not be removed from this.jobManagerInventory and this.cache.\n */\nSupervisor.prototype.clearUnusedJobManagersAndModuleCache = function Supervisor$clearUnusedJobManagersAndModuleCache(newJobKeys=[])\n{\n let deleteCount = this.jobManagerInventory.length - this.cachedJobsThreshold;\n if (deleteCount > 0)\n {\n selectiveDebugging && console.debug(`Supervisor.clearUnusedJobManagersAndModuleCache START: deleteCount ${deleteCount}/${this.jobManagerInventory.length}/${this.cachedJobsThreshold}.`);\n const jobMap = {};\n newJobKeys.forEach(jobAddress => { jobMap[jobAddress] = 1; });\n for (const jobManager of this.jobManagerInventory)\n {\n if (!jobMap[jobManager.address])\n {\n const sliceInventory = jobManager.sliceInventory.filter(slice => slice.isActive || slice.isQueued);\n if (sliceInventory.length < 1)\n {\n this.purgeJob(jobManager);\n if (--deleteCount < 1)\n break;\n }\n }\n }\n selectiveDebugging && console.debug(`Supervisor.clearUnusedJobManagersAndModuleCache FINISH: deleteCount ${deleteCount}/${this.jobManagerInventory.length}/${this.cachedJobsThreshold}.`);\n }\n}\n\n/**\n * Ask the scheduler (task distributor) for work.\n * @param {number} [unusedSandboxSlots]\n * @param {object[]} [jobs=[]] \n */\nSupervisor.prototype.requestTask = function Supervisor$requestTask (unusedSandboxSlots, jobs = [])\n{\n if (!this.isReady() || this.isFetchingNewWork)\n return Promise.resolve();\n\n if(!unusedSandboxSlots) unusedSandboxSlots = this.unusedSandboxSlots();\n if (unusedSandboxSlots < 1)\n {\n debugging('supervisor') && console.debug('requestTask: There are no unused sandbox slots.');\n return Promise.resolve();\n }\n\n // Refresh connections.\n this.instantiateAllConnections();\n\n // We prune for over this.maxTotalSandboxes about every 15 seconds, or when must prune level is reached.\n if (this.sandboxCount() > this.mustPruneSandboxLevel || Date.now() > this.lastPrune + this.pruneFrequency)\n {\n this.lastPrune = Date.now();\n this.pruneSandboxes();\n }\n\n try\n {\n this.isFetchingNewWork = true;\n const numCPUSlotToFill = this.numberOfAvailableSandboxSlots(unusedSandboxSlots);\n if (numCPUSlotToFill < 1)\n {\n //debugging() && console.debug('Predicted workload too high; not fetching additional work yet'); <-- Save Wes' msg...\n debugging() && console.debug('Supervisor.requestTask: We have enough, so start executing some slices.');\n return this.roundRobinSlices(); // roundRobinSlices guarantees this.isFetchingNewWork === false\n }\n\n /** XXXpfr @todo Get together with Wes to figure this out. */\n //let predictedLoad = this.predictLoad(Date.now() + ms(this.tuning.prefetchInterval)).load;\n\n const request = {\n numCores: numCPUSlotToFill,\n coreStats: this.getStatisticsCPU(),\n numGPUs: this.numGPU,\n //targetLoad: this.targetLoad.subtract(predictedLoad), /** XXXpfr @todo Get together with Wes to figure this out. */\n conversionQuantum: this.conversionQuantum(),\n capabilities: this.capabilities,\n paymentAddress: this.paymentAddress,\n jobAddresses: jobs.concat(this.options.jobAddresses || []), // When set, only fetches slices for these jobs.\n localExec: this.options.localExec,\n workerComputeGroups: this.generateWorkerComputeGroups(),\n minimumWage: workerTuning.minimumWage || this.options.minimumWage,\n loadedJobs: this.jobManagerInventory.map(jobMan => jobMan.address),\n readyJobs: this.jobManagerInventory.filter(jobMan => jobMan.ready).map(jobMan => jobMan.address),\n previouslyWorkedJobs: this.ringBufferofJobs.buf, // Only discrete jobs.\n rejectedJobs: this.rejectedJobs,\n };\n // Workers should be part of the public compute group by default.\n if (!booley(workerTuning.leavePublicGroup) && !booley(this.options.leavePublicGroup))\n request.workerComputeGroups.push(constants.computeGroups.public);\n\n // Call Task Distributor and handle response with this.addTaskToWorkload.\n this.fetchTask(request, (response) => this.addTaskToWorkload(request, response));\n }\n catch (error)\n {\n // Paranoid double-checking we don't accidently leave a live this.isFetchingNewWork.\n this.isFetchingNewWork = false;\n throw error;\n }\n}\n\n/** Gets the logical and physical number of cores and also the total number of sandboxes the worker is allowed to run. */\nSupervisor.prototype.getStatisticsCPU = function Supervisor$getStatisticsCPU ()\n{\n if (DCP_ENV.isBrowserPlatform)\n {\n return {\n worker: this.workerId,\n lCores: window.navigator.hardwareConcurrency,\n pCores: workerTuning.pCores || window.navigator.hardwareConcurrency,\n sandbox: this.maxWorkingSandboxes,\n }\n }\n\n return {\n worker: this.workerId,\n lCores: requireNative('os').cpus().length,\n pCores: requireNative('physical-cpu-count'),\n sandbox: this.maxWorkingSandboxes,\n }\n}\n\n/**\n * Callback for fetchTask.\n * @param {object} request \n * @param {object} response\n */\nSupervisor.prototype.addTaskToWorkload = function Supervisor$addTaskToWorkload (request, response)\n{\n try\n {\n const payload = response.payload;\n if (!response.success)\n {\n debugging() && console.debug('Task fetch failure; request=', request);\n debugging() && console.debug('Task fetch failure; response=', payload);\n // Only report errors when in 'ready' state.\n if (this.isReady()) throw new DCPError('Unable to fetch task for worker', payload);\n else return;\n }\n\n const sliceCount = payload.body.task.length || 0;\n if (sliceCount < 1)\n {\n // Display completed results so far.\n if (selectiveDebugging && this.queuedSliceCount() < 1)\n {\n const values = Object.values(this.resultMap);\n if (values.length > 0)\n {\n values.forEach((descriptor) => descriptor.slices.sort((x,y) => x-y))\n console.debug(`Recorded results: job managers ${values.length}:`, this.resultMap);\n }\n }\n this.emit('fetchedTask', { jobs: 0, slices: sliceCount });\n // There may be an extra slice to process.\n // roundRobinSlices guarantees this.isFetchingNewWork === false;\n return this.roundRobinSlices();\n }\n\n //console.log('addTaskToWorkload:3', Date.now() - this.lastTime); // SAVE\n\n /**\n * payload structure: { owner: this.address, signature: signature, auth: messageLightWeight, body: messageBody };\n * messageLightWeight: { workerId: worker, jobSlices, schedulerId, jobCommissions }\n * messageBody: { newJobs: await-getNewJobsForTask(dbScheduler, task, request), task }\n */\n const { body, ...authorizationMessage } = payload;\n /** @type {{ newJobs: object, task: SliceMessage[] }} */\n const { newJobs, task } = body;\n assert(newJobs); // It should not be possible to have !newJobs -- we throw on !success.\n const newJobKeys = Object.keys(newJobs);\n const jobCount = newJobKeys.length;\n\n /*\n * Ensure all jobs received from the scheduler (task distributor) are:\n * 1. If we have specified specific jobs the worker may work on, the received jobs are in the specified job list\n * 2. If we are in localExec, at most 1 unique job type was received (since localExec workers are designated for only one job)\n * If the received jobs are not within these parameters, stop the worker since the scheduler cannot be trusted at that point.\n */\n if (request.jobAddresses.length > 0 && !newJobKeys.every((ele) => request.jobAddresses.includes(ele))\n || request.localExec && jobCount > 1)\n {\n // Sup1 did not guard this diag.\n console.error(\"Worker received slices it shouldn't have. Rejecting the work and stopping.\");\n process.exit(1);\n }\n\n selectiveDebugging && console.debug(`Supervisor.addTaskToWorkload: task: ${task.length}/${this.maxWorkingSandboxes}, conversion: ${request.conversionQuantum}, jobs: ${jobCount}, authSlices: ${compressJobMap(authorizationMessage.auth.authSlices, true /* skipFirst*/, addressTruncationLength /* digits*/)}`);\n\n //console.log('addTaskToWorkload:4', Date.now() - this.lastTime); // SAVE\n\n // Clear out job managers w/o any queued slices,\n // and remove corresponding job references from module cache.\n // When a cached module no longer has any job references it is removed from the cache.\n this.clearUnusedJobManagersAndModuleCache(newJobKeys);\n\n // this.jobMap: job.address --> jobManager\n /** @type {Object.<Address, JobManager>} */\n this.jobMap = {};\n this.jobManagerInventory.forEach(jobManager => {\n this.jobMap[jobManager.address] = jobManager;\n });\n\n /** @type {Object.<Address, SliceMessage[]>} */\n const jobSliceMap = {};\n task.forEach((element) => {\n const address = String(element.jobAddress);\n if (!jobSliceMap[address]) jobSliceMap[address] = [element];\n else jobSliceMap[address].push(element);\n });\n\n debugging('supervisor') && console.debug('requestTask: slices, newJobs and jobMap', task.length, Object.keys(newJobs), Object.keys(this.jobMap));\n\n // Populate the job managers with slices, creating new job managers when necessary.\n // Set up discrete job ring buffer.\n for (const [jobAddress, jobEl] of Object.entries(newJobs))\n {\n if (this.jobMap.hasOwnProperty(jobAddress))\n this.jobMap[jobAddress].update(jobEl, jobSliceMap[jobAddress], authorizationMessage);\n else\n {\n // Add the slice messages to the job manager ctor, so that slice construction is after job manager is ready.\n const jobManager = new JobManager(this, jobEl, jobSliceMap[jobAddress], authorizationMessage);\n this.jobMap[jobAddress] = jobManager;\n this.jobManagerInventory.push(jobManager);\n\n // Populate the ring buffer based on job's discrete property.\n assert(jobEl.requirements);\n if (jobEl.requirements.discrete && this.ringBufferofJobs.find(address => address === jobEl.address) === undefined)\n this.ringBufferofJobs.push(jobEl.address);\n }\n }\n\n /**\n * The requestTask event fires when the supervisor has finished trying to\n * fetch work from the scheduler (task distributor). The data emitted is the\n * number of jobs and new slices to work on in the fetched task.\n *\n * @event Supervisor#requestTask\n * @type {object}\n */\n this.emit('fetchedTask', { jobs: jobCount, slices: sliceCount });\n\n //console.log('addTaskToWorkload:5', Date.now() - this.lastTime); // SAVE\n\n // Start working on the slices.\n setImmediate(() => this.roundRobinSlices());\n }\n catch (error)\n {\n this.isFetchingNewWork = false; // Paranoid double checking that we don't leave this.isFetchingNewWork live.\n this.emit('fetchTaskFailed', error);\n debuggingError && console.error('Supervisor.requestTask failed!', error);\n }\n}\n\n/**\n * Returns the number of unused sandbox slots to fill -- sent to requestTask.\n * @returns {number}\n */\nSupervisor.prototype.numberOfAvailableSandboxSlots = function Supervisor$numberOfAvailableSandboxSlots(unusedSandboxSlots)\n{\n const _readySlices = this.readySlices();\n let numCores;\n if (this.options.priorityOnly && this.options.jobAddresses.length === 0)\n numCores = 0;\n else if (_readySlices.length > 1) // We have slices ready, no need to fetch.\n numCores = 0;\n else\n {\n // There are almost no ready slices (there may be 0 or 1), fetch a full task.\n // The task is full, in the sense that it will contain slices whose\n // aggregate execution time is this.maxWorkingSandboxes * 5-minutes.\n // However, there can only be unusedSandboxSlots # of long slices.\n // Thus we need to know whether the last slice in this.readySlices() is long or not.\n // (A long slice has estimated execution time >= 5-minutes on an average worker.)\n const longSliceCount = (_readySlices.length > 0 && _readySlices[0].isLong) ? 1 : 0;\n numCores = unusedSandboxSlots - longSliceCount;\n }\n return numCores;\n}\n\n/**\n * @callback cbAddTaskToWorkload\n * @param {Response} response\n * @returns {Promise<void>}\n */\n\n/**\n * Call to fetch new slices from task distributor.\n * @param {*} request\n * @param {cbAddTaskToWorkload} addTaskToWorkload\n * @returns {Promise<void>}\n */\nSupervisor.prototype.fetchTask = async function Supervisor$fetchTask (request, addTaskToWorkload)\n{\n // Fetch a new task if we have insufficient slices queued, then start workers\n assert(this.isFetchingNewWork);\n\n this.instantiateAllConnections();\n\n // Top up sandboxes when necessary.\n const moreSandboxes = this.maxWorkingSandboxes - this.sandboxCount();\n if (moreSandboxes > 0)\n {\n // If the evaluator cannot start (e.g. if the evalServer is not running),\n // then the while loop will keep retrying until the evalServer comes online.\n while (true)\n {\n debugging('supervisor') && console.debug(`Supervisor.work: creating ${moreSandboxes} sandboxes; currently have ${this.maxWorkingSandboxes-moreSandboxes}`);\n try\n {\n await this.createNewSandbox(); // Place new sandbox in this.readiedSandboxes.\n if (moreSandboxes > 1)\n this.createAndSaveSandboxes(moreSandboxes - 1);\n break;\n }\n catch (error)\n {\n // Sup1 did not guard this diag.\n console.warn('Failed to ready sandboxes; will retry.', this.checkCode(error));\n await a$sleepMs(1000);\n }\n }\n\n this.checkCapabilities();\n }\n\n const fetchTimeout = setTimeout(() => {\n this.isFetchingNewWork = false;\n // Sup1 did not guard this diag.\n console.warn(`Fetch exceeded timeout, will reconnect at next watchdog interval`);\n safeClose(this.taskDistributor, 'Fetch timed out', Math.random() > 0.5).catch(error => {\n console.error(`Failed to close task-distributor connection`, error); // Sup1 did not guard this diag.\n });\n safeClose(this.resultSubmitter, 'Fetch timed out', Math.random() > 0.5).catch(error => {\n console.error(`Failed to close result-submitter connection`, error); // Sup1 did not guard this diag.\n });\n this.instantiateAllConnections();\n }, 3 * 60 * 1000); // Max out at 3 minutes to fetch.\n\n // Ensure result submitter and task distributor connections before fetching tasks.\n try\n {\n await this.resultSubmitter.keepalive();\n await this.taskDistributor.keepalive();\n }\n catch (e)\n {\n this.isFetchingNewWork = false;\n console.error('Failed to connect to result submitter, refusing to fetch slices. Will try again at next fetch cycle.'); // Sup1 did not guard this diag.\n debugging('supervisor') && console.debug('Error:', e);\n clearTimeout(fetchTimeout);\n safeClose(this.taskDistributor, 'Failed to connect to result-submitter', true).catch(error => {\n console.error(`Failed to close task-distributor connection`, error); // Sup1 did not guard this diag.\n });\n safeClose(this.resultSubmitter, 'Failed to connect to result-submitter', true).catch(error => {\n console.error(`Failed to close result-submitter connection`, error); // Sup1 did not guard this diag.\n });\n return Promise.resolve();\n }\n\n this.emit('fetchingTask');\n\n if (!this.taskDistributor) return\n return this.taskDistributor.send('requestTask', request)\n .then((response) => {\n addTaskToWorkload(response);\n })\n .catch((error) => {\n this.isFetchingNewWork = false; // Redundant.\n this.emit('fetchTaskFailed', error);\n console.error('Unable to request task from scheduler. Will try again on a new connection.', error); // Sup1 did not guard this diag.\n safeClose(this.taskDistributor, error, true);\n })\n .finally(() => {\n this.isFetchingNewWork = false;\n clearTimeout(fetchTimeout);\n });\n}\n\n/**\n * Generate the workerComputeGroups property of the requestTask message. \n * \n * Concatenate the compute groups object from dcpConfig with the list of compute groups\n * from the supervisor, and remove the public group if accidentally present. Finally,\n * we transform joinSecrets/joinHash into joinHashHash for secure transmission.\n *\n * @note computeGroup objects with joinSecrets are mutated to record their hashes. This\n * affects the supervisor options and dcpConfig. Re-adding a joinSecret property\n * to one of these will cause the hash to be recomputed.\n */\nSupervisor.prototype.generateWorkerComputeGroups = function Supervisor$generateWorkerComputeGroups ()\n{\n let computeGroups = Object.values(workerTuning.computeGroups || {});\n if (this.options.computeGroups)\n computeGroups = computeGroups.concat(this.options.computeGroups);\n computeGroups = computeGroups.filter(group => group.id !== constants.computeGroups.public.id);\n const hashedComputeGroups = [];\n for (const group of computeGroups)\n {\n const groupCopy = Object.assign({}, group);\n if ((group.joinSecret || group.joinHash) && (!group.joinHashHash || this.lastDcpsid !== this.taskDistributor.dcpsid))\n {\n let joinHash;\n if (group.joinHash)\n joinHash = group.joinHash.replace(/\\s+/g, ''); // strip whitespace\n else\n joinHash = calculateJoinHash(groupCopy);\n\n groupCopy.joinHashHash = hash.calculate(hash.eh1, joinHash, this.taskDistributor.dcpsid);\n delete groupCopy.joinSecret;\n delete groupCopy.joinHash;\n debugging('computeGroups') && console.debug(`Calculated joinHash=${joinHash} for`, groupCopy);\n }\n hashedComputeGroups.push(groupCopy);\n }\n this.lastDcpsid = this.taskDistributor.dcpsid;\n debugging('computeGroups') && console.debug('Requesting ', computeGroups.length, 'non-public groups for session', this.lastDcpsid);\n return hashedComputeGroups;\n}\n\n// _Idx\n//\n// Aggregators from the job managers.\n// Note: Not all functions are used yet.\n//\n/** XXXpfr @todo Figure out which aggregators to keep. */\n\n/**\n * Gather the count of job managers with queuedSlices.\n * @returns {number}\n */\nSupervisor.prototype.activeJobCount = function Supervisor$activeJobCount ()\n{\n let count = 0;\n this.jobManagerInventory.forEach((jobManager) => {\n if (jobManager.queuedSlices.length > 0) count++;\n });\n return count;\n}\n\n/**\n * Gather the ready slices from the job managers.\n * @returns {Slice[]}\n */\nSupervisor.prototype.readySlices = function Supervisor$readySlices ()\n{\n const readySlices = [];\n this.jobManagerInventory.forEach((jobManager) => {\n readySlices.push(...jobManager.readySlices);\n });\n return readySlices;\n}\n\n/**\n * Gather the working slices in the job managers.\n * @returns {Slice[]}\n */\nSupervisor.prototype.workingSlices = function Supervisor$workingSlices ()\n{\n const workingSlices = [];\n this.jobManagerInventory.forEach((jobManager) => {\n workingSlices.push(...jobManager.workingSlices);\n });\n return workingSlices;\n}\n\n/**\n * Gather the count of various kinds of slices over all the job managers.\n * @param {string} predicate - 'all;, 'ready', 'queued', 'reserved', 'working', 'workingOnly'.\n * @returns {number}\n */\nSupervisor.prototype.predicateSliceCount = function Supervisor$predicateSliceCount (predicate)\n{\n let count = 0;\n switch (predicate)\n {\n case 'all':\n this.jobManagerInventory.forEach((jobManager) => {\n count += jobManager.sliceInventory.length;\n });\n break\n case 'ready':\n this.jobManagerInventory.forEach((jobManager) => {\n count += jobManager.readySlices.length;\n });\n break;\n case 'queued':\n this.jobManagerInventory.forEach((jobManager) => {\n count += jobManager.queuedSlices.length;\n });\n break;\n case 'reserved':\n this.jobManagerInventory.forEach((jobManager) => {\n count += jobManager.reservedSlices.length;\n });\n break;\n case 'working': // both working and reserved (viz., soon-to-be-working)\n this.jobManagerInventory.forEach((jobManager) => {\n count += jobManager.workingSlices.length;\n });\n break;\n case 'workingOnly':\n this.jobManagerInventory.forEach((jobManager) => {\n count += jobManager.workingSlicesOnly.length;\n });\n break;\n }\n return count;\n}\n/** @returns {number} */\nSupervisor.prototype.sliceCount = function Supervisor$sliceCount () { return this.predicateSliceCount('all'); }\n/** @returns {number} */\nSupervisor.prototype.readySliceCount = function Supervisor$readySliceCount () { return this.predicateSliceCount('ready'); }\n/** @returns {number} */\nSupervisor.prototype.queuedSliceCount = function Supervisor$queuedSliceCount () { return this.predicateSliceCount('queued'); }\n/** @returns {number} */\nSupervisor.prototype.reservedSliceCount = function Supervisor$reservedSliceCount () { return this.predicateSliceCount('reserved'); }\n/** @returns {number} */\nSupervisor.prototype.workingSliceCount = function Supervisor$workingSliceCount () { return this.predicateSliceCount('working'); }\n/** @returns {number} */\nSupervisor.prototype.workingSliceOnlyCount = function Supervisor$workingSliceOnlyCount () { return this.predicateSliceCount('workingOnly'); }\n\n/**\n * Gather the count of working sandboxes over all the job managers.\n * @returns {number}\n */\nSupervisor.prototype.sandboxCount = function Supervisor$sandboxCount ()\n{\n return this.readiedSandboxes.length + this.sandboxInventory.filter((sandbox) => !sandbox.isTerminated).length;\n}\n\n/**\n * Gather the count of working sandboxes over all the job managers.\n * @returns {number}\n */\nSupervisor.prototype.workingSandboxCount = function Supervisor$workingSandboxCount ()\n{\n return this.sandboxInventory.filter((sandbox) => !sandbox.isTerminated && sandbox.isWorking).length;\n}\n\n// _Idx\n//\n// Sandbox creation and management.\n// \n\n/**\n * Create and start a Sandbox.\n * When this.readiedSandboxes.length > 0, use one of those sandboxes, instead of creating a new one.\n * @param {number} [delayMs=0] - The delay ms when calling sandbox.start(delayMs) .\n * @returns {Promise<Sandbox>}\n */\nSupervisor.prototype.createSandbox = function Supervisor$createSandbox (delayMs = 0)\n{\n if (this.readiedSandboxes.length > 0)\n {\n const sandbox = this.readiedSandboxes.pop();\n this.sandboxInventory.push(sandbox);\n return Promise.resolve(sandbox);\n }\n // Do not place in this.readiedSandboxes, we'll directly use the return value of createSandbox.\n return this.createNewSandbox(delayMs, true/*putInInventory*/);\n}\n \n/**\n * Create and start a Sandbox.\n * Store it in this.readiedSandboxes.\n * @param {boolean} [putInInventory=false]\n * @param {number} [delayStartMs=0] - Millisecond delay when calling sandbox.start(delayStartMs), otherwise return it and use it.\n * @returns {Promise<Sandbox>}\n */\nSupervisor.prototype.createNewSandbox = function Supervisor$createNewSandbox (delayStartMs = 0, putInInventory = false)\n{\n const rawSandbox = new Sandbox(this.cache, { ...this.options.sandboxOptions }, this.allowedOrigins);\n this.hookUpSandboxListeners(rawSandbox);\n return rawSandbox.start(delayStartMs)\n .then((sandbox) => {\n if (putInInventory) this.sandboxInventory.push(sandbox);\n else this.readiedSandboxes.push(sandbox);\n return sandbox;\n })\n .catch((error) => {\n if (!error) error = new Error('Unknown error creating sandbox.');\n debuggingWarn && console.warn(`Supervisor.createNewSandbox: Failed to start sandbox ${rawSandbox.identifier}.`, error);\n rawSandbox.terminate(false);\n if (error.code === 'ENOWORKER')\n throw new DCPError(\"Cannot use localExec without dcp-worker installed. Use the command 'npm install dcp-worker' to install the neccessary modules.\", 'ENOWORKER');\n throw error;\n });\n}\n\n/**\n * Bulk: create and start Sandboxes and save in this.readiedSandboxes.\n * @param {number} numSandboxes - The number of sandboxes to create.\n * @returns {Promise<void>}\n */\nSupervisor.prototype.createAndSaveSandboxes = async function Supervisor$createAndSaveSandboxes (numSandboxes)\n{\n const promises = [], errors = [];\n let first = true;\n for (let k = 0; k < numSandboxes; k++)\n {\n const promise = this.createNewSandbox(first ? 0: this.delayMs())\n .catch((error) => errors.push(this.checkCode(error)));\n promises.push(promise);\n first = false;\n }\n\n await Promise.all(promises);\n\n if (errors.length) // Sup1 did not guard this diag.\n console.warn(`Failed to ready ${errors.length} of ${numSandboxes} sandboxes.`, errors);\n\n // Sort so that pop() will return sandboxes in increasing order.\n this.readiedSandboxes.sort((x,y) => y.id - x.id);\n\n debugging('supervisor') && console.debug(`createAndSaveSandboxes: Created ${numSandboxes-errors.length} sandboxes.`, this.readiedSandboxes.map(s => s.id));\n}\n\n/**\n * For a given sandbox, hook up all the Sandbox listeners.\n * @param {Sandbox} sandbox \n */\nSupervisor.prototype.hookUpSandboxListeners = function hookUpSandboxListeners (sandbox) \n{\n sandbox.addListener('ready', () => this.emit('sandboxReady', sandbox));\n\n sandbox.addListener('start', () => {\n this.emit('sandboxStart', sandbox);\n\n if (sandbox.slice)\n {\n try\n {\n const statusPayload = sandbox.slice.getMessagePayload(this.workerId, 'begin');\n this.resultSubmitter.send('status', statusPayload).catch((error) => {\n debuggingError && console.error(`Error sending 'status' for slice ${sandbox.slice.identifier}:\\n\\t${error}\\n\\tWill try again on a new connection`);\n this.resultSubmitterMessageQueue.push({ operation: 'status', data: statusPayload });\n safeClose(this.resultSubmitter);\n });\n } \n catch (error)\n {\n /* resultSubmitterConnection can be null if worker is stopped */\n debuggingError && console.error(`Failed to send 'begin' status for slice ${sandbox.slice.identifier}, no connection to result submitter`, error);\n }\n }\n });\n\n sandbox.addListener('workEmit', ({ eventName, payload }) => {\n // Need to check if the sandbox hasn't been assigned a slice yet.\n if (!sandbox.slice)\n {\n // Sup1 did not guard this diag.\n console.error(`Sandbox not assigned a slice before sending workEmit message to scheduler.\\n\\t'workEmit' event originates from '${eventName}' event`, payload);\n }\n else\n {\n const slice = sandbox.slice;\n // Sometimes a sliceNumber===0 workEmit comes in before the client bundle is properly loaded.\n // Also happens with minor dcp-client version mismatches.\n // sliceNumber===0 <==> authorizationMessage undefined...\n if (!slice.authorizationMessage) // Sup1 did not guard this diag.\n console.warn(`workEmit: missing authorization message for slice ${slice.identifier}`);\n else if (this.eventRouter) // No reason to emit if event router is closed.\n {\n const workEmitPayload = {\n eventName,\n payload,\n job: slice.jobAddress,\n slice: slice.sliceNumber,\n worker: this.workerId,\n authorizationMessage : slice.authorizationMessage,\n };\n\n const workEmitPromise = this.eventRouter.send('workEmit', workEmitPayload).catch(error => {\n debuggingWarn && console.warn(`workEmit: Unable to send ${eventName} for slice ${slice.identifier}: ${error.message}.\\n\\tTrying again on a new connection.`);\n this.eventRouterMessageQueue.push({ operation: 'workEmit', data: workEmitPayload })\n safeClose(this.eventRouter); // stopWork could slip-in during eventRouter.send\n if (this.debugBuild) console.error('workEmit error:', error); // Sup1 did not guard this diag.\n });\n\n if (this.debugBuild)\n {\n workEmitPromise.then(result => { // Sup1 did not guard this diag.\n if (!result || !result.success) /*debuggingWarn &&*/ console.warn('workEmit: Event router did not accept event', result);\n });\n }\n }\n }\n });\n\n sandbox.on('rejectedWorkMetrics', (data) => {\n // If the slice already has rejectedTimeReport, add this data to it. If not, assign this data to slices rejectedTimeReport property\n if (sandbox.slice) \n {\n if (!sandbox.slice.rejectedTimeReport) sandbox.slice.rejectedTimeReport = data.timeReport;\n else \n {\n ['total', 'CPU', 'webGL'].forEach((key) => {\n if (data.timeReport[key]) sandbox.slice.rejectedTimeReport[key] += data.timeReport[key];\n })\n }\n }\n });\n\n // If the sandbox terminated and we are not shutting down, then we should return all work which is\n // currently not being computed if all sandboxes are dead and the attempt to create a new one fails.\n sandbox.on('terminated',async () => {\n let nonTerminatedSandboxes = this.sandboxInventory.filter(sbx => !sbx.isTerminated);\n if (nonTerminatedSandboxes.length === 0 && this.worker.working)\n {\n debugging('supervisor') && console.debug(`hookUpSandboxListeners: Try to create 1 sandbox in the sandbox-terminated-handler.`);\n let _sandbox;\n try\n {\n _sandbox = await this.createNewSandbox(); // Place in this.readiedSandboxes.\n } catch (e) {}\n\n // If we cannot create a new sandbox, that probably means we're on a screensaver worker\n // and the screensaver is down. So return the slices to the scheduler.\n if (!_sandbox) this.screenSaverDestroy();\n }\n });\n}\n\n/**\n * Terminate extra sandboxes over the limit: this.maxTotalSandboxes.\n * First terminate assigned sandboxes which are unlikely to be used with the current ready slices.\n * Then terminate the unassigned sandboxes: this.readiedSandboxes.\n * (There should be no readied sandboxes at this point.)\n * Then round-robin prune 1 assigned sandbox from each jobmanager.\n * XXXpfr @todo Prioritize sandboxes that we wish to keep.\n * E.g. When a sandbox is especially expensive to assign.\n */\nSupervisor.prototype.pruneSandboxes = function Supervisor$pruneSandboxes () \n{\n let pruneCount = this.sandboxCount() - this.maxTotalSandboxes;\n if (pruneCount <= 0) return;\n selectiveDebugging && console.debug(`Supervisor.pruneSandboxes START: pruneCount ${pruneCount}/${this.sandboxCount()}/${this.maxTotalSandboxes}.`);\n // Only prune the extras: jm.assignedSandboxes.length > jm.queuedSlices.length .\n // Round-robin prune 1 extra assigned sandbox from each jobmanager.\n const readyJobManagers = this.jobManagerInventory.filter(jm => jm.ready);\n while (true)\n {\n const _pruneCount = pruneCount;\n for (const jm of readyJobManagers)\n {\n if (jm.pruneExtraAssignedSandbox())\n {\n if (--pruneCount < 1)\n {\n selectiveDebugging && console.debug(`Supervisor.pruneSandboxes FINISH: unpruned ${pruneCount}/${this.sandboxCount()}/${this.maxTotalSandboxes}.`);\n return;\n }\n }\n }\n if (pruneCount === _pruneCount)\n break;\n }\n assert(pruneCount > 0);\n // Prune the excess non-assigned sandboxes -- we should never hit this.\n if (this.readiedSandboxes.length > 0)\n {\n const toPrune = this.readiedSandboxes.slice(0, pruneCount);\n this.readiedSandboxes = this.readiedSandboxes.slice(pruneCount);\n toPrune.forEach(sandbox => sandbox.terminate(false));\n pruneCount -= toPrune.length;\n if (pruneCount < 1)\n {\n selectiveDebugging && console.debug(`Supervisor.pruneSandboxes FINISH: unpruned ${pruneCount}/${this.sandboxCount()}/${this.maxTotalSandboxes}.`);\n return;\n }\n }\n // Round-robin prune 1 assigned sandbox from each jobmanager.\n while (true)\n {\n const _pruneCount = pruneCount;\n for (const jm of readyJobManagers)\n {\n if (jm.pruneAssignedSandbox())\n {\n if (--pruneCount < 1)\n {\n selectiveDebugging && console.debug(`Supervisor.pruneSandboxes FINISH: unpruned ${pruneCount}/${this.sandboxCount()}/${this.maxTotalSandboxes}.`);\n return;\n }\n }\n }\n if (pruneCount === _pruneCount)\n break;\n }\n this.sandboxInventory = this.sandboxInventory.filter((sandbox) => !sandbox.isTerminated);\n selectiveDebugging && console.debug(`Supervisor.pruneSandboxes FINISH: unpruned ${pruneCount}/${this.sandboxCount()}/${this.maxTotalSandboxes}.`);\n}\n\n// _Idx\n//\n// Result-submitter-result support functions.\n// Send in the results!!!\n//\n\n/**\n * Submits the slice results to the result-submitter service.\n * Then remove the slice from the its job manager.\n *\n * @param {Slice} slice - The slice to submit.\n * @returns {Promise<void>}\n */\nSupervisor.prototype.recordResult = function Supervisor$recordResult (slice)\n{\n // It is possible for slice.result to be undefined when there are upstream errors.\n if ( !(slice && slice.result))\n throw new Error(`recordResult: slice.result is undefined for slice ${slice.identifier}. This is ok when there are upstream errors.`); \n if (!slice.isComplete)\n throw new Error('Cannot record result for slice that has not completed execution successfully.');\n\n debugging('supervisor') && console.debug(`supervisor: recording result for slice ${slice.identifier}.`);\n\n /* @see result-submitter::result for full message details */\n const metrics = { GPUTime: 0, CPUTime: 0, CPUDensity: 0, GPUDensity: 0, total: 0 };\n const payloadData = {\n slice: slice.sliceNumber,\n job: slice.jobAddress,\n worker: this.workerId,\n paymentAddress: this.paymentAddress,\n metrics,\n authorizationMessage: slice.authorizationMessage,\n }\n\n const timeReport = slice.timeReport;\n if (timeReport)\n {\n debugging('supervisor') && console.debug('recordResult timeReport', timeReport);\n // If slice takes less than 1ms to execute, CPUTime will be 0, so compensate.\n if (timeReport.CPU < 1)\n {\n timeReport.CPU++;\n timeReport.total++;\n }\n if (timeReport.total < timeReport.CPU + timeReport.webGL)\n {\n // Compensate or throw? For now we compensate.\n debuggingWarn && console.warn(`Supervisor.recordResult:: Inconsistent time report -- total < CPU + webGL -- ${stringify(timeReport)}`)\n //throw new Error(`recordResult: Inconsistent time report -- total < CPU + webGL -- ${stringify(timeReport)}`)\n timeReport.total = timeReport.CPU + timeReport.webGL;\n }\n if (timeReport.total > 0)\n {\n slice.jobManager.updateStatistics(timeReport);\n metrics.total = timeReport.total;\n metrics.CPUTime = timeReport.CPU;\n metrics.GPUTime = timeReport.webGL;\n metrics.CPUDensity = metrics.CPUTime / timeReport.total;\n metrics.GPUDensity = metrics.GPUTime / timeReport.total;\n }\n }\n\n this.emit('submittingResult');\n\n if (!this.resultSubmitter)\n this.connectTo('resultSubmitter');\n\n if (slice.resultStorageType === 'pattern')\n return this.sendResultToRemote(slice)\n .then((response) => {\n payloadData.result = response;\n this.sendToResultSubmitter(slice, payloadData);\n });\n\n payloadData.result = encodeDataURI(slice.result.result);\n return this.sendToResultSubmitter(slice, payloadData);\n}\n\n/**\n * @param {Slice} slice\n * @param {*} payloadData\n * @returns {Promise<void>}\n */\nSupervisor.prototype.sendToResultSubmitter = async function Supervisor$sendToResultSubmitter (slice, payloadData)\n{\n try\n {\n debugging('supervisor') && console.debug('Supervisor.recordResult: payloadData', payloadData.result.slice(0, 256), slice.identifier);\n\n return await this.resultSubmitter.send('result', payloadData)\n .then((resp) => {\n if (!resp.success)\n throw resp.payload;\n\n debugging('supervisor') && console.debug('recordResult: SUCCESS', slice.identifier);\n\n const receipt = {\n accepted: true,\n payment: resp.payload.slicePaymentAmount,\n };\n this.emit('submittedResult', resp.payload);\n this.emit('dccCredit', receipt);\n });\n }\n catch (error)\n {\n // Sup1 did not guard this diag.\n console.error(`Failed to submit results to scheduler for slice ${payloadData.slice} of job ${payloadData.job}`, error?error:'');\n //slice.jobManager.dumpSlices('recordResult');\n this.resultSubmitterMessageQueue.push({ operation: 'result', data: payloadData });\n const msg = safeCloseEx('resultSubmitter', this.resultSubmitter);\n if (!error && msg) error = new Error(msg); // eslint-disable-line no-ex-assign\n this.emit('submitSliceFailed', error);\n }\n finally\n {\n slice.markAsFinished();\n this.emit('submitFinished');\n // Remove the slice from the job manager.\n slice.jobManager.removeSlice(slice);\n if (this.sliceTiming)\n {\n slice['resultDelta'] = Date.now() - slice['resultDelta'];\n console.debug(`recordResult(${slice['queueingDelta']}, ${slice['executionDelta']}, ${slice['resultDelta']}): Completed slice ${slice.identifier}.`);\n }\n if (selectiveDebugging)\n {\n if (!this.resultMap[slice.jobAddress]) this.resultMap[slice.jobAddress] = { slices: [], totalTimes: [] };\n this.resultMap[slice.jobAddress].slices.push(slice.sliceNumber);\n this.resultMap[slice.jobAddress].totalTimes.push(payloadData.metrics.total);\n }\n }\n}\n\n/**\n * Send a work function's result to a server that speaks our DCP Remote Data Server protocol.\n * @param {Slice} slice - Slice object whose result we are sending.\n * @returns {Promise<string>}\n * @throws When HTTP status not in the 2xx range.\n */\nSupervisor.prototype.sendResultToRemote = function Supervisor$sendResultToRemote (slice)\n{ \n // Construct postParams.\n const postParams = { ...slice.resultStorageParams };\n postParams.element = slice.sliceNumber;\n /** Currently data will be outputed as a JSON object, XXXpfr @todo: Support file upload and other contentTypes. */\n postParams.contentType = 'application/json';\n\n const result = slice.result.result;\n if (result) postParams.content = JSON.stringify(result);\n else postParams.error = JSON.stringify(slice.error);\n debugging('supervisor') && console.debug('sendResultToRemote: content: ', (result ? postParams.content : postParams.error).slice(0, 512));\n\n // Construct url.\n const sliceResultUri = makeDataURI('pattern', slice.resultStorageDetails, {\n slice: slice.sliceNumber,\n job: slice.jobAddress,\n });\n debugging() && console.debug('sendResultToRemote sliceResultUri: ', sliceResultUri);\n const url = new DcpURL(sliceResultUri);\n\n // Check allowed origins.\n if (this.allowedOrigins.indexOf(url.origin) === -1 && this.sendResults.indexOf(url.origin) === -1)\n throw new Error(`Invalid origin for remote result storage: '${url.origin}'`);\n\n return justFetch(url, 'JSON', 'POST', false, postParams)\n .then((response) => encodeDataURI(JSON.stringify(response)));\n}\n\n// _Idx\n//\n// Reject.\n//\n\n/**\n * Handles reassigning or returning a slice that was rejected by a sandbox.\n *\n * If the slice does not have a rejected property already, reassign the\n * slice to a new sandbox and add a rejected property to the slice to\n * indicate it has already rejected once.\n *\n * If the slice rejects with a reason, or has a rejected time stamp\n * (ie. has been rejected once already) then return all slices from the\n * job to the scheduler and terminate all sandboxes with that jobAddress.\n *\n * The sandbox will be terminated.\n *\n * @param {Sandbox} sandbox\n * @param {Slice} slice\n */\nSupervisor.prototype.handleWorkReject = async function Supervisor$handleWorkReject (sandbox, slice, rejectReason)\n{\n debugging() && console.debug('handleWorkReject', rejectReason, slice.rejectedTimeStamp, slice.identifier);\n\n // Do a hard flush of the microtask queue and finish the current event loop.\n await new Promise((resolve) => setImmediate(() => setTimeout(resolve, 0)));\n\n const jobManager = slice.jobManager;\n jobManager.rejectedJobReasons.push(rejectReason); // memoize reasons\n\n // First time rejecting without a reason. Try assigning slice to a new sandbox.\n if (rejectReason === 'false' && !slice.rejectedTimeStamp)\n {\n // Set rejected time stamp.\n slice.rejectedTimeStamp = Date.now();\n // Schedule the slice for execution.\n jobManager.scheduleSlice(slice, true /* placeInTheFrontOfTheQueue*/);\n // Slice has been rescheduled, but we still need to terminate the sandbox.\n jobManager.returnSandbox(sandbox);\n }\n else\n { \n // Slice has a reason OR rejected without a reason already and got stamped.\n // Add to array of rejected jobs.\n let rejectedJob = {\n address: slice.jobAddress,\n reasons: jobManager.rejectedJobReasons,\n }\n this.rejectedJobs.push(rejectedJob);\n\n // Purge the job.\n this.purgeJob(jobManager);\n\n // Tell everyone all about it, when allowed.\n if (jobManager.displayMaxDiagInfo)\n {\n const suffixMsg = '\\n\\tAll slices with the same jobAddress returned to the scheduler.\\n\\tAll sandboxes with the same jobAddress are terminated.';\n if (slice.rejectedTimeStamp)\n console.warn(`work.reject: The slice ${slice.identifier} was rejected twice.${suffixMsg}`);\n else\n console.warn(`work.reject: The slice ${slice.identifier} was rejected with reason ${rejectReason}.${suffixMsg}`);\n }\n }\n}\n\n// _Idx\n//\n// Unused functions that we need to review.\n// 1) destroy, shutdown, halt -- possibly need to incorporate these ideas in stopWork\n// 2) predictLoad -- XXXpfr: I really feel bad about not being able to figure out how to incorporate\n// this into the design of sup2. This was a central part of Wes' design of sup2.\n// I need to collaborate with Wes to resolve my ignorance.\n//\n\n/**\n * UNUSED\n * @deprecated -- may use later\n **/\nSupervisor.prototype.destroy = function Supervisor$destory()\n{\n selectiveDebugging && console.debug(`Supervisor.screenSaverDestroy: destroying Supervisor and everything else.`);\n this.stopWork(true /*forceTerminate*/);\n if (this.state) this.state.destroy();\n if (this.progressReportTimer) clearInterval(this.progressReportTimer);\n if (this.watchdogTimer) clearInterval(this.watchdogTimer);\n this.state = null;\n this.progressReportTimer = null;\n this.watchdogTimer = null;\n this.jobManagerInventory = null;\n this.sandboxInventory = [];\n this.readiedSandboxes = [];\n this.closeConnections();\n}\n\n/**\n * UNUSED\n * @deprecated -- may use later \n * Halt the Supervisor as quickly as possible.\n **/\nSupervisor.prototype.halt = function Supervisor$halt()\n{\n this.state.setIf('ready', 'stopping');\n if (!this.state.is('stopping'))\n throw new Error(`Supervisor has an invalid state ${this.state} for halt`);\n clearInterval(this.watchdogTimer);\n\n for (let jobMan of this.jobManagerInventory)\n {\n jobMan.state.setIf('ready', 'stop');\n for (const sandbox of jobMan.workingSandboxes)\n {\n sandbox.stop(); // NYI -- will terminate.\n }\n }\n}\n \n/**\n * UNUSED\n * @deprecated -- may use later \n * Shutdown the supervisor; attempts to return work which will not be finished before timeout expires.\n * The shutdown is complete once this supervisor emits the stopped state change.\n */\nSupervisor.prototype.shutdown = function Supervisor$shutdown(timeoutMs)\n{\n var ps = [], returnSliceInventory = [];\n var timer;\n\n this.state.setIf('ready', 'stopping');\n if (!this.state.is('stopping'))\n throw new Error(`Supervisor has an invalid state ${this.state} for shutdown`);\n clearInterval(this.watchdogTimer);\n\n for (let jobMan of this.jobManagerInventory)\n {\n jobMan.state.setIf('ready', 'stop');\n\n for (let slice of jobMan.sliceInventory)\n {\n if (slice.state.is('initial') || slice.state.is('ready'))\n {\n returnSliceInventory.push(slice);\n }\n else if (slice.state.is(sliceStatus.working))\n {\n ps.push(new Promise((resolve, reject) => {\n slice.state.on('change', (status) => {\n if (status === 'done')\n resolve();\n });\n }));\n }\n }\n }\n\n const reason = 'Supervisor.shutdown';\n this.returnSlices(returnSliceInventory, reason);\n timer = setTimeout(this.halt.bind(this), timeoutMs);\n Promise.all(ps)\n .then(() => {\n clearTimeout(timer);\n this.state.set('stopping', 'stopped');\n })\n .catch((e) => {\n if (e.code !== 'DCP_SUPERVISOR_ESYNC')\n throw e; /* becomes unhandled rejection */\n });\n}\n\n/** \n * Factory function which generates a list of origins which are safe to communicate \n * with for this purpose. Currently-valid purposes (more will be added):\n * - any\n * - fetchData\n * - fetchWorkFunctions\n * - fetchArguments\n * - sendResults\n */\nSupervisor.prototype.makeSafeOriginList = function Supervisor$$makeSafeOriginList(purpose)\n{\n var list = [];\n \n if (this[purpose])\n list = list.concat(this[purpose]);\n \n /* Add 'any' origin(s) to list iff not in localExec, or in localExec and purpose is sendResults */\n if (!this.options.localExec || (this.options.localExec && purpose === 'sendResults'))\n list = list.concat(this.allowedOrigins)\n \n return list;\n}\n \n /**\n * UNUSED -- DOES NOT WORK YET.\n * NEED TO WORK WITH WES TO FIGURE OUT BEST WAY TO GET PREDICTLOAD TO WORK.\n * Predict the load on this supervisor based on the local job measurement data.\n * Works by looking at current conditions and available slices, and tries to guess\n * in what order they will be finished, working, etc. \n *\n * The simulation is very naive, but is expected to be accurate several seconds\n * into the future, particularly as we approach the end of a task.\n *\n * @param {number} whenMs \n * \n * @returns {Object<load, jobManagerInventory>} where load is and instance of Load and the predicted \n * load at the prediction time, and jobManagerInventory \n * is a counterfeit which holds the predicted state of \n * the jobManagerInventory at that time.\n */\nSupervisor.prototype.predictLoad = function Supervisor$predictLoad (whenMs)\n{\n /** @type {JobManager[]} */\n var jmi = new Inventory(); /* Inventory of counterfeit JobManagers. */\n var load = new Load(0,0); /* This \"current\" load throughout the prediction. */\n /** @type {Slice} */\n var next; /* The next slice to \"finish\". */\n\n /* Initialize data structures for prediction from current activity. */\n for (let jobMan of this.jobManagerInventory.filter(jm => jm.state.is('ready') && jm.sliceInventory.length))\n {\n jobMan = jobMan.counterfeit();\n jmi.push(jobMan);\n jobMan.sliceInventory.forEach((s) => s.state.setIf('initial', 'ready'));\n }\n next = findNextSlice();\n \n /**\n * Routine that finds the slice that will end next (soonest.)\n * @returns {Slice}\n */\n function findNextSlice()\n {\n /** @type {Slice} */\n var _next;\n for (let jobMan of jmi)\n {\n const _workingSlices = jobMan.workingSlices;\n for (let slice of _workingSlices)\n {\n //\n // slice.etaMs is the estimated time interval until slice execution completes.\n //\n // If the slice hasn't started,\n // slice.etaMs = slice.jobManager.estimateWallMs,\n // else if the slice has completed execution:\n // slice.etaMs = 0.\n // else if the slice has started:\n // slice.jobManager.estimateWallMs - (Date.now() - slice.startTime).\n //\n if (_next && (_next.etaMs <= slice.etaMs))\n continue;\n\n _next = slice;\n }\n }\n load.add(_next.jobManager.metrics);\n \n return _next;\n }\n\n /* At this point, jmi is an Inventory of counterfeit job managers that are \"ready\" for\n * work, next.etaMs is the time interval until the next slice will finish, and we have\n * a reasonably accurate picture of our current load.\n *\n * Next, we \"end\" this slice, try to fill all cores, and push the timeline forward to\n * the next predicted end of slice.\n */\n for (next = findNextSlice();\n next && (next.etaMs < whenMs);\n next = findNextSlice())\n {\n let ended = next;\n let cursor = this.makeJobSelectionCursor(jmi);\n\n /* \"end\" this slice */\n load.subtract(ended.jobManager.metrics);\n /* Fake out collecting result to transition state to FINISHED. */\n ended.collectResult(null);\n\n /* \"start\" as many slices as we can - given our CPU/GPU constraints, slice data in memory, etc */\n while (this.targetLoad.fits(load))\n {\n let slice = cursor.next();\n if (!slice)\n break; /* Running out of work that fits. */\n\n if (!load.fits(this.targetLoad, slice.jobManager.metrics))\n continue;\n\n /* Pick a ready slice from this job and add its anticipated load to our current load if it will fit */\n slice = slice.jobManager.readySlices.shift();\n slice.markAsWorking(); // ?? Not sure this is correct.\n //slice.etaMs = ended.etaMs + slice.jobManager.estimateWallMs; wtf?!?! <--- LOOK HERE\n\n load.add(slice.jobManager.metrics);\n }\n }\n\n return { load, jobManagerInventory: jmi };\n}\n\n\n//# sourceURL=webpack://dcp/./src/dcp-client/worker/supervisor2/index.js?");
4527
+
4528
+ /***/ }),
4529
+
4530
+ /***/ "./src/dcp-client/worker/supervisor2/job-manager.js":
4531
+ /*!**********************************************************!*\
4532
+ !*** ./src/dcp-client/worker/supervisor2/job-manager.js ***!
4533
+ \**********************************************************/
4534
+ /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
4535
+
4536
+ "use strict";
4537
+ eval("/**\n * @file dcp-client/worker/supervisor2/job-manager.js\n *\n * A support class for Supervisor2.\n * It is a wrapper for the job object returned from the requestTask,\n * along with tracking slices and sandboxes associated with the job. \n *\n * @author Wes Garland, wes@kingsds.network,\n * Paul, paul@kingsds.network,\n * @date Dec 2020,\n * June 2022,\n * @module JobManager\n */\n\n/* global dcpConfig */ // eslint-disable-line no-redeclare\n// @ts-check\n\n\n/** @typedef {import('./').Supervisor} Supervisor */\n/** @typedef {import('./sandbox2').Sandbox} Sandbox */\n/** @typedef {import('dcp/common/dcp-url').DcpURL} DcpURL */\n/** @typedef {import('dcp/utils').SliceMessage} SliceMessage */\n/** @typedef {string} opaqueId */ // 22 character base64 string \n/** @typedef {string} address */ // String(Address)\n\nconst inspect = Symbol.for('nodejs.util.inspect.custom');\nconst debugging = (__webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope)('worker');\nconst { EventEmitter } = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\");\nconst { assert } = __webpack_require__(/*! dcp/common/dcp-assert */ \"./src/common/dcp-assert.js\");\nconst { Inventory } = __webpack_require__(/*! dcp/utils/inventory */ \"./src/utils/inventory.js\");\nconst { Synchronizer } = __webpack_require__(/*! dcp/common/concurrency */ \"./src/common/concurrency.js\");\nconst { Slice } = __webpack_require__(/*! ./slice2 */ \"./src/dcp-client/worker/supervisor2/slice2.js\");\nconst { Load } = __webpack_require__(/*! ./load */ \"./src/dcp-client/worker/supervisor2/load.js\");\nconst { Statistics } = __webpack_require__(/*! ./rolling-statistics */ \"./src/dcp-client/worker/supervisor2/rolling-statistics.js\");\nconst DCP_ENV = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\nconst { rehydrateRange } = __webpack_require__(/*! dcp/dcp-client/range-object */ \"./src/dcp-client/range-object.js\");\nconst kvin = __webpack_require__(/*! kvin */ \"./node_modules/kvin/kvin.js\");\nconst scopedKvin = new kvin.KVIN({Object: ({}).constructor,\n Array: ([]).constructor, \n Function: (()=>{}).constructor});\nconst { fetchURI, dumpObject, truncateAddress, stringify } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\n\nconst addressTruncationLength = 20;\nlet workerTuning = dcpConfig.worker;\nif (!workerTuning) workerTuning = dcpConfig.Supervisor;\nif (!workerTuning || !workerTuning.maxSandboxErrorsPerSlice || !workerTuning.allowConsoleAccess)\n workerTuning = { maxSandboxErrorsPerSlice: 2, allowConsoleAccess: false };\n\n// This is for debugging and performance testing.\n// Several functions chain and return non-awaited promises.\n// When AWAIT_ALL is true, we await the promises, which makes easier debugging.\nconst AWAIT_ALL = false;\n\n// Debug tracing helpers.\nconst selectiveEnable = false;\nconst displayWarnError = false || selectiveEnable;\nconst selectiveDebugging = selectiveEnable || debugging();\nconst debuggingError = false || selectiveDebugging || displayWarnError;\nconst debuggingWarn = false || selectiveDebugging || displayWarnError;\nconst selectiveDebugging2 = selectiveEnable && false || debugging('jobmanager');\n\nconst INITIAL = 'initial';\nconst READY = 'ready';\nconst STOP = 'stop';\nconst REFUSE = 'refuse';\nconst BROKEN = 'broken';\n\n//\n// Index to functionality -- search for '_Idx' to toggle through the index.\n//\n// 1) Ctor: JobManager class definition and various properties.\n// 2) Statistics: updateStatistics, update.\n// 3) Dtors: destroy, stopWork.\n// 4) Assign sandboxes and execute slices.\n// 5) Remove from array and return sandbox.\n// 6) Fetch: WorkFn, slices, arguments.\n// 7) Miscellaneous.\n// 8) Unused functions that we need to review.\n//\n\n// _Idx\n//\n// Ctor: JobManager class definition and various properties.\n// A JobManager handles all scheduling of slices for a given job.\n// It's also responsible for fetching slice data, work functions and arguments.\n// And it collects statistics about slice completion times and resource usage.\n// All functionality across jobs is handled by Supervisor.\n//\n\n/**\n * JobManager Constructor. An instance of JobManager knows everything about a given\n * job within the supervisor, including:\n * - work function code\n * - work function arguments\n * - all the SliceManager instances that go with this job\n * - how long a slice of this job is expected to run\n *\n * Instances of JobManager emit the following events:\n * - addSlice(sliceHandle)\n * - deleteSlice(sliceHandle)\n * - statusChange(new state, old state);\n *\n * JobManager States\n *\n * Start state:\n * initial\n *\n * Intermediate states:\n * ready\n *\n * Terminal states:\n * broken - job could not be initialized\n * refuse - for some reason, we have decided that we don't want work for this job\n * stop - job manager has been stopped\n *\n * Valid transitions:\n * initial -> broken\n * initial -> ready\n * ready -> stop\n * \n * NOTE: If you ever use a property with a leading underscore you are probably making a mistake.\n * But if you must, please ask paul, yarn, bryan or eddie for a CR.\n */\nclass JobManager extends EventEmitter\n{\n /**\n * @constructor\n * @param {Supervisor} parent - Owning Supervisor.\n * @param {object} jobMessage - Job Descriptor from getNewJobsForTask.\n * @param {SliceMessage[]} sliceMessages - Messages from task distributor describing slices.\n * @param {object} authorizationMessage - The signature that shipped with the task authorizing this worker.\n */\n constructor(parent, jobMessage, sliceMessages, authorizationMessage)\n {\n super('JobManager');\n /** @type {Supervisor} */\n this._supervisor = parent;\n /** @type {Slice[]} */\n this._sliceInventory = []; // All slices for this.address.\n /** @type {Sandbox[]} */\n this._sandboxInventory = []; // All sandboxes for this.address.\n /** @type {Synchronizer} */\n this._state = new Synchronizer(INITIAL, [ INITIAL, READY, STOP, REFUSE, BROKEN ]);\n this.state.on('change', (neu, old) => this.emit('statusChange', neu, old));\n\n /** XXXpfr @todo Is there any reason to do a deeper clone here. */\n this.jobMessage = { ...jobMessage };\n\n /** @type {string} */\n this._address = String(this.jobMessage.address);\n /** @type {Load} */\n this._load = null;\n /** @type {Statistics} */\n this._statistics = new Statistics(0.25);\n /** @type {number} */\n this._emaSliceTime = 0;\n /** @type {string[]} */\n this.rejectedJobReasons = [];\n /** @type {boolean} */\n this.isEstimation = true;\n\n const that = this;\n /** \n * Start loading dependencies in the background. Once these are loaded, this.state will \n * transition to 'ready' and the job will be ready for work.\n */\n (async function supervisor$$JobManager$$loadDependencies() {\n await that.fetchJob(that.jobMessage);\n })(/* iife */)\n .then (() => {\n debugging('jobmanager') && console.debug('JobManager is transitioning to READY', this.identifier);\n this.state.set(INITIAL, READY);\n this.addSlices(sliceMessages, authorizationMessage);\n })\n .catch((error) => {\n debuggingError && console.error('fetchJob has failed', error);\n this.state.set(INITIAL, BROKEN);\n })\n .finally(() => {\n selectiveDebugging && console.debug('JobManager.loadDependencies completed.', this.identifier);\n });\n }\n\n /** @type {string} */\n get address () { return this._address; }\n /** @type {string} */\n get uuid () { return this.jobMessage.uuid; }\n /** @type {boolean} */\n get workerConsole () { return this.jobMessage.workerConsole; }\n /** @type {object} */\n get requirements () { return this.jobMessage.requirements; }\n\n // These 3 properties have type object.\n get mro () { return this.jobMessage.mro; }\n get arguments () { return this.jobMessage.arguments; }\n get workFunction () { return this.jobMessage.workFunction; }\n\n /** @type {{computeGroups: Array<{opaqueId: string, name: string, description: string}>, name: string, description: string, link: string}} */\n get public () { return this.jobMessage.public; }\n /** @type {{sliceCPUTime: number, sliceGPUTime: number, sliceCPUDensity: number, sliceGPUDensity: number}} */\n get metrics () { return this.jobMessage.metrics; }\n /** @type {Supervisor} */\n get supervisor () { return this._supervisor; }\n /** @type {Synchronizer} */\n get state () { return this._state; }\n\n /** @type {boolean} */\n get initial () { return this.state.is(INITIAL); }\n /** @type {boolean} */\n get ready () { return this.state.is(READY); }\n /** @type {boolean} */\n get stopped () { return this.state.is(STOP); }\n /** @type {boolean} */\n get refuse () { return this.state.is(REFUSE); }\n /** @type {boolean} */\n get broken () { return this.state.is(BROKEN); }\n\n /** @type {Sandbox[]} */\n get sandboxInventory () { return this._sandboxInventory; }\n /** @type {Sandbox[]} */\n get safeSandboxes () { return this.sandboxInventory.filter((sandbox) => !sandbox.isTerminated); }\n /** @type {Sandbox[]} */\n get assignedSandboxes () { return this.sandboxInventory.filter((sandbox) => sandbox.isAssigned); }\n /** @type {Sandbox[]} */\n get workingSandboxes () { return this.sandboxInventory.filter((sandbox) => sandbox.isWorking); }\n /** @type {Sandbox[]} */\n get nonWorkingSandboxes () { return this.sandboxInventory.filter((sandbox) => !sandbox.isWorking && !sandbox.isTerminated); }\n /** @type {Slice[]} */\n get sliceInventory () { return this._sliceInventory; }\n /** @type {Slice[]} */\n get readySlices () { return this.sliceInventory.filter((slice) => slice.isReady); }\n /** @type {Slice[]} - ready and soon-to-be-ready */\n get queuedSlices () { return this.sliceInventory.filter((slice) => slice.isQueued); }\n /** @type {Slice[]} */\n get reservedSlices () { return this.sliceInventory.filter((slice) => slice.isReserved); }\n /** @type {Slice[]} - unassigned, ready and reserved */\n get activeSlices () { return this.sliceInventory.filter((slice) => slice.isActive); }\n /** @type {Slice[]} - working and soon-to-be-working */\n get workingSlices () { return this.sliceInventory.filter((slice) => slice.isWorking || slice.isReserved); }\n /** @type {Slice[]} - working only */\n get workingSlicesOnly () { return this.sliceInventory.filter((slice) => slice.isWorking); }\n\n /** @type {Load} */\n get load () { return this._load; }\n /** @type {number} */\n get delayMs () { return this.supervisor.delayMs(); }\n /** @type {Statistics} */\n get statistics () { return this._statistics; }\n /** @type {number} */\n get emaSliceTime () { return this._emaSliceTime; }\n /** @type {number} */\n get globalTime () { \n const denominator = this.metrics.sliceCPUDensity + this.metrics.sliceGPUDensity;\n return denominator > 0 ? (this.metrics.sliceCPUTime + this.metrics.sliceGPUTime) / denominator : 300 * 1000;\n }\n /** @type {string} */\n get identifier () { return `${truncateAddress(this.address, addressTruncationLength)}.${this.state}`; }\n /** @type {string} */\n get [inspect] () { return `[Object JobManager <${this.public.name}::${this.address}::${this.state}>]`; }\n /** \n * Estimate the wall time (i.e. actual elapsed time on a wall clock) for a slice of this job.\n * We pick the ema time (alpha=0.25) + 0.7 * stddev to incorporate uncertainty.\n * About 76% of slice completion times are <= ema + 0.7 * stddev.\n * This estimate is very primitive and assumes that CPU and GPU code do not run at the same\n * time.\n *\n * @type {number} - estimated time period in milliseconds\n */ \n get estimateWallMs () { return this.emaSliceTime + 0.7 * this.statistics.stddev; } /** XXXpfr @todo make this.statistics.stddev reflect this.globalTime too. */\n /** @type {boolean} */\n get debugBuild () { return this.supervisor.debugBuild; }\n /**\n * Always display max info under debug builds, otherwise maximal error\n * messages are displayed to the worker, only if both worker and client agree.\n * @type {boolean} - When true, display stack trace and other enhanced diag info.\n **/\n get displayMaxDiagInfo () { return this.workerConsole && workerTuning.allowConsoleAccess || this.debugBuild; }\n\n // _Idx\n //\n // Statistics: updateStatistics, update.\n //\n\n /** @param {{ total, CPU, webGL }} timeReport */\n updateStatistics (timeReport)\n {\n this.statistics.add(timeReport.total);\n this._emaSliceTime = 0.5 * (this._emaSliceTime + this.statistics.ema); /** XXXpfr @todo double smoothing, need to test and compare against this.statistics.ema . */\n debugging('jobmanager') && console.debug('JobManager.updateStatistics: mean', this.statistics.mean, 'stddev', this.statistics.stddev, 'ema', this.statistics.ema, 'ma', this.statistics.ma, 'x', this.statistics.x);\n }\n\n /**\n * Update jobMessage, add some slices to inventory and possibly update the initial seed of the statistics.\n * @param {object} jobMessage - Job Descriptor from getNewJobsForTask.\n * @param {SliceMessage[]} sliceMessages - Messages from task distributor describing slices.\n * @param {object} authorizationMessage - The signature that shipped with the task authorizing this worker.\n */\n update (jobMessage, sliceMessages, authorizationMessage)\n {\n this.jobMessage = Object.assign(this.jobMessage, { ...jobMessage });\n this.addSlices(sliceMessages, authorizationMessage);\n if (!this._emaSliceTime || this.statistics.count < 1)\n this._emaSliceTime = this.globalTime;\n // this._load = new Load(this.metrics.sliceCPUTime, this.metrics.sliceGPUTime); // SAVE for Sup2 Part II\n }\n\n // _Idx\n //\n // Dtors: destroy, stopWork.\n //\n\n /**\n * Destructor.\n */\n destroy()\n {\n selectiveDebugging && console.debug(`JobManager.destroy: terminating sandboxes and returning slices to scheduler for job manager ${this.identifier}.`);\n \n this.sandboxInventory.forEach((sandbox) => {\n if (!sandbox.isTerminated) sandbox.terminate(false);\n });\n this._sandboxInventory = [];\n\n const slicesToReturn = this.sliceInventory.filter((slice) => !slice.isFinished);\n const reason = `JobManager destroy: ${this.identifier}`;\n this.supervisor.returnSlices(slicesToReturn, reason, false /*removeSlices*/);\n this._sliceInventory = [];\n }\n\n /**\n * Terminates sandboxes and returns slices.\n * Sets the working flag to false, call @this.work to start working again.\n * \n * If forceTerminate is true: Terminates all sandboxes and returns all slices.\n * If forceTerminate is false: Terminates non-working sandboxes and returns queued slices.\n *\n * @param {boolean} [forceTerminate = true] - true if you want to stop the sandboxes from completing their current slice.\n */\n stopWork (forceTerminate = true)\n {\n selectiveDebugging && console.debug(`JobManager.stopWork(${forceTerminate}): terminating sandboxes and returning slices to scheduler.`);\n if (forceTerminate)\n {\n this.destroy();\n }\n else\n {\n // Return all non-finished slices which are not working.\n const reason = `JobManager stopWork(false): ${this.identifier}`;\n this.supervisor.returnSlices(this.queuedSlices, reason);\n this._sliceInventory = [];\n\n // Terminate all sandboxes that are not working.\n let idleSandboxes = this.sandboxInventory.filter(w => !w.isWorking);\n for (const sandbox of idleSandboxes)\n this.returnSandbox(sandbox);\n this._sandboxInventory = [];\n }\n }\n \n /**\n * Terminate 1 assigned sandbox when there are more than threshold assigned sandboxes.\n * @param {number} [threshold=0] - integer >= 0\n * @returns {boolean}\n */\n pruneAssignedSandbox(threshold = 0)\n {\n const _assignedSandboxes = this.assignedSandboxes;\n if (_assignedSandboxes.length > threshold)\n {\n debugging('supervisor') && console.debug(`JobManager.pruneAssignedSandbox terminate ${_assignedSandboxes[0].identifier}, threshold ${threshold}`);\n _assignedSandboxes[0].markAsUnready(); // Remove from this.assignedSandboxes.\n this.returnSandbox(_assignedSandboxes[0]);\n return true;\n }\n return false;\n }\n\n /**\n * Terminate 1 assigned sandbox when there are more than this.queuedSlices.length assigned sandboxes.\n * @returns {boolean}\n */\n pruneExtraAssignedSandbox()\n {\n return this.pruneAssignedSandbox(this.queuedSlices.length);\n }\n\n // _Idx\n //\n // Work: Assign sandboxes and execute slices.\n //\n // hookUpSandboxListeners handles the sandbox complete event and tries to schedule another slice\n // from the same job on the sandbox. If there aren't any slices from the same job, it tries slices\n // from other job managers, and if there are no other ready slices, it calls Supervisor.requestTask\n // to get more work from TD .\n //\n // Several functions chain and return non-awaited promises.\n // When AWAIT_ALL is true, we await the promises, which makes easier debugging.\n //\n\n /**\n * Hook up JobManager specific Sandbox event listenters.\n * @param {Sandbox} sandbox\n */\n hookUpSandboxListeners(sandbox)\n {\n // Sandbox error handler.\n sandbox.on('sandboxError', (error) => JobManager.handleSandboxError(this, sandbox, error));\n // Sandbox complete handler.\n // When any sandbox completes, try to run another slice in the same jobManager.\n // Otherwise, go through the Supervisor.requestTask protocol.\n sandbox.addListener('complete', () => {\n if (this.supervisor.isReady())\n {\n const _readySlices = this.readySlices;\n if (this.supervisor.runSliceFromSameJob() && _readySlices.length > 0)\n {\n const slice = _readySlices[0];\n slice.markAsReserved();\n this.runSlice(slice, this.delayMs);\n }\n else if (!this.supervisor.isFetchingNewWork)\n {\n const unusedSandboxSlots = this.supervisor.unusedSandboxSlots();\n if (unusedSandboxSlots > 0)\n this.supervisor.requestTask(unusedSandboxSlots);\n }\n }\n });\n }\n\n /**\n * Create a Sandbox.\n * Start it. Assign it. Add it to jobManager inventory.\n * @param {number} delayStartMs - The delay ms to pass to sandbox.start .\n * @param {number} delayAssignMs - The delay ms to pass to sandbox.assign .\n * @returns {Promise<Sandbox>}\n */\n async readySandbox (delayStartMs, delayAssignMs)\n {\n const sandbox = await this.supervisor.createSandbox(delayStartMs);\n this.hookUpSandboxListeners(sandbox);\n return sandbox.assign(this, delayAssignMs)\n .catch((error) => { // error could be a string coming from evaluator\n const errorObj = { jobAddress: truncateAddress(this.address, addressTruncationLength), error: this.checkStackTrace(error) };\n console.error('Failed to assign job to sandbox.', errorObj);\n this.returnSandbox(sandbox);\n throw error;\n });\n }\n\n /**\n * Mark both sandbox and slice as working.\n * @param {Sandbox} sandbox\n * @param {Slice} slice\n */\n markAsWorking(sandbox, slice)\n {\n assert(sandbox);\n assert(slice);\n slice.markAsWorking();\n sandbox.markAsWorking(slice);\n }\n\n /**\n * Create or reuse a sandbox and run a slice on it.\n * @param {Slice} slice - The slice to execute on a sandbox.\n * @param {number} [delayMs=0] - The delay ms to pass to sandbox.start .\n * @returns {Promise<void>}\n */\n async runSlice(slice, delayMs)\n {\n assert(slice);\n if (this.supervisor.sliceTiming) slice['queueingDelta'] = Date.now();\n\n const _assignedSandboxes = this.assignedSandboxes;\n if (AWAIT_ALL)\n {\n if (_assignedSandboxes.length > 0 && this.supervisor.readiedSandboxes.length < 1)\n {\n const sandbox = _assignedSandboxes[0];\n this.markAsWorking(sandbox, slice);\n return await this.runSliceOnSandbox(slice, sandbox, this.delayMs); \n }\n try\n {\n const sandbox = await this.readySandbox(delayMs/*delayStartMs*/, 0.5 * this.delayMs/*delayAssignMs*/);\n this.sandboxInventory.push(sandbox);\n this.markAsWorking(sandbox, slice);\n return await this.runSliceOnSandbox(slice, sandbox, this.delayMs);\n }\n catch (error)\n {\n // Any exception from readySandbox will have already been emitted.\n debugging('jobmanager') && console.error(`JobManager.runSlice: Failure trying to run slice ${slice.identifier} on a sandbox.`, error);\n this.supervisor.returnSlice(slice, 'EUNCAUGHT');\n }\n }\n else\n {\n if (_assignedSandboxes.length > 0 && this.supervisor.readiedSandboxes.length < 1)\n {\n const sandbox = _assignedSandboxes[0];\n this.markAsWorking(sandbox, slice);\n return this.runSliceOnSandbox(slice, sandbox, this.delayMs); \n }\n return this.readySandbox(delayMs/*delayStartMs*/, this.delayMs/*delayAssignMs*/)\n .then((sandbox) => {\n this.sandboxInventory.push(sandbox);\n this.markAsWorking(sandbox, slice);\n return this.runSliceOnSandbox(slice, sandbox, this.delayMs);\n })\n .catch((error) => {\n // Any exception from readySandbox will have already been emitted.\n debugging('jobmanager') && console.error(`JobManager.runSlice: Failure trying to run slice ${slice.identifier} on a sandbox.`, error);\n this.supervisor.returnSlice(slice, 'EUNCAUGHT');\n })\n }\n }\n\n /**\n * Execute slice on sandbox, collect results or handle errors, and clean up.\n * @param {Slice} slice - The slice to execute on the sandbox.\n * @param {Sandbox} sandbox - The sandbox on which to execute the slice.\n * @param {number} [delayMs=0] - The delay ms to pass to sandbox.work .\n * @returns {Promise<void>}\n */\n async runSliceOnSandbox(slice, sandbox, delayMs = 0)\n {\n selectiveDebugging2 && console.debug(`runSliceOnSandbox ${sandbox.identifier}~${slice.identifier} #readySlices ${this.readySlices.length}/${this.supervisor.readySliceCount()}, #SBs, ${this.sandboxInventory.length}/${this.supervisor.sandboxCount()}, #working(w/r/wo/wsbx), ${this.workingSlices.length}/${this.reservedSlices.length}/${this.workingSlicesOnly.length}/${this.workingSandboxes.length}, #sup-working(w/r/wo/wsbx), ${this.supervisor.workingSliceCount()}/${this.supervisor.reservedSliceCount()}/${this.supervisor.workingSliceOnlyCount()}/${this.supervisor.workingSandboxCount()}, #assigned, ${this.assignedSandboxes.length}, #readiedSBs, ${this.supervisor.readiedSandboxes.length}, #localSBs, ${this.sandboxInventory.map(s => Number(s.id)).sort((x,y)=>x-y)}`);\n //console.log('runSliceOnSandbox', Date.now() - this.supervisor.lastTime, slice.identifier); // SAVE temporarily\n assert(slice && sandbox);\n slice.verifyWorking();\n sandbox.verifyWorking();\n\n if (this.supervisor.sliceTiming)\n {\n slice['queueingDelta'] = Date.now() - slice['queueingDelta'];\n slice['executionDelta'] = Date.now();\n }\n slice.startTime = Date.now();\n\n if (AWAIT_ALL)\n {\n try\n {\n const result = await sandbox.work(slice, delayMs);\n debugging('jobmanager') && console.debug(`runSliceOnSandbox - success: ${sandbox.id}~${slice.identifier}`, result);\n slice.collectResult(result, true);\n sandbox.changeWorkingToAssigned();\n this.supervisor.recordResult(slice);\n }\n catch (error)\n {\n const reason = await this.supervisor.handleSandboxWorkError(sandbox, slice, error);\n debuggingError && console.error(`runSliceOnSandbox - failure: ${sandbox.id}~${slice.identifier}`, reason, error);\n }\n finally\n {\n slice.startTime = null;\n if (this.supervisor.sliceTiming)\n {\n slice['executionDelta'] = Date.now() - slice['executionDelta'];\n slice['resultDelta'] = Date.now();\n }\n }\n }\n else\n {\n return sandbox.work(slice, delayMs)\n .then((result) => {\n debugging('jobmanager') && console.debug(`runSliceOnSandbox - success: ${sandbox.id}~${slice.identifier}`, result);\n slice.collectResult(result, true);\n sandbox.changeWorkingToAssigned();\n this.supervisor.recordResult(slice);\n })\n .catch(async (error) => {\n const reason = await this.supervisor.handleSandboxWorkError(sandbox, slice, error);\n debuggingError && console.error(`runSliceOnSandbox - failure: ${sandbox.id}~${slice.identifier}`, reason, error);\n })\n .finally(() => {\n slice.startTime = null;\n if (this.supervisor.sliceTiming)\n {\n slice['executionDelta'] = Date.now() - slice['executionDelta'];\n slice['resultDelta'] = Date.now();\n }\n });\n }\n }\n\n /**\n * Sandbox has had an error which is not from the work function: terminate it\n * and try to redo the slice.\n * @param {JobManager} jobManager \n * @param {Sandbox} sandbox \n * @param {Error} error \n */\n static handleSandboxError (jobManager, sandbox, error)\n {\n const slice = sandbox.slice;\n if (slice)\n {\n if (!slice.isWorking) // Sanity. Exception should never fire.\n throw new Error(`handleSandboxError: slice ${slice.identifier} must be WORKING.`);\n\n slice['sandboxErrorCount'] = (slice['sandboxErrorCount'] || 0) + 1;\n sandbox.slice = null;\n if (slice['sandboxErrorCount'] < (workerTuning.maxSandboxErrorsPerSlice || 2))\n slice.resetState();\n else\n {\n slice.collectResult(error, false /*success*/);\n const reason = `sandboxError event handler, too many errors ${slice['sandboxErrorCount']}: ${sandbox.id}~${jobManager.identifier}`;\n jobManager.supervisor.returnSlice(slice, reason);\n }\n //debuggingWarn && -- Sup1 does not guard this\n console.warn(`JobManager.handleSandboxError: Sandbox ${sandbox.identifier}...(${sandbox.public.name}/${slice['sandboxErrorCount']}) with slice ${slice.identifier} had error.`, error);\n }\n else\n //debuggingError && -- Sup1 does not guard this\n console.error(`JobManager.handleSandboxError: Sandbox ${sandbox.identifier} has no slice...(${sandbox.public.name} had error.`, error);\n\n jobManager.returnSandbox(sandbox); /* terminate the sandbox */\n }\n\n // _Idx\n //\n // Remove from array and return sandbox.\n //\n\n /**\n * Remove sandbox from this.sandboxInventory and terminate is not already terminated.\n * @param {Sandbox} sandbox - The sandbox to remove from this.sandboxInventory.\n * @param {boolean} [assertExists=true] - When true (and type=array) then assert that sandbox exists in this.sandboxInventory.\n */\n returnSandbox(sandbox, assertExists = true)\n {\n debugging('supervisor') && console.debug(`JobManager.returnSandbox: ${sandbox.identifier}, assertExists ${assertExists}`);\n if (this.sandboxInventory && this.sandboxInventory.length > 0)\n JobManager.removeElement(this.sandboxInventory, sandbox, assertExists);\n // The cleaning up of terminated sandboxes in this.supervisor.sandboxInventory happens in this.supervisor.purgeSandboxes.\n if (!sandbox.isTerminated)\n sandbox.terminate(false);\n }\n /**\n * Remove slice from this.sliceInventory.\n * @param {Slice} slice - The slice to remove from this.sliceInventory.\n * @param {boolean} [assertExists=true] - When true (and type=array) then assert that slice exists in this.sliceInventory.\n */\n removeSlice(slice, assertExists = true)\n {\n assert(this.sliceInventory instanceof Array);\n if (this.sliceInventory && this.sliceInventory.length > 0)\n JobManager.removeElement(this.sliceInventory, slice, assertExists);\n }\n\n /**\n * Remove slices from this.sliceInventory.\n * @param {Slice[]} slices - The slices to remove from this.sliceInventory.\n */\n removeSlices(slices)\n {\n assert(this.sliceInventory instanceof Array);\n if (this.sliceInventory && this.sliceInventory.length > 0)\n this._sliceInventory = this.sliceInventory.filter((slice) => slices.indexOf(slice) === -1);\n }\n\n /**\n * Remove element from arrayLike.\n * @param {Array<*>} array\n * @param {object|number} element\n * @param {boolean} [assertExists=true]\n */\n static removeElement(array, element, assertExists = true)\n {\n let index = array.indexOf(element);\n if (assertExists)\n {\n assert(index !== -1);\n array.splice(index, 1);\n } else if (index !== -1) array.splice(index, 1);\n }\n\n // _Idx\n //\n // Fetch: WorkFn, slices, arguments.\n //\n\n /**\n * Fetch work function, work function arguments and possibly the range object describing the jobs slices..\n * @param {object} mpe - messagePayloadElement: job object returned by task-jobs.js . \n */\n async fetchJob(mpe)\n {\n if (debugging('worker'))\n dumpObject(mpe, 'JobManager.fetchJob: mpe', 512);\n\n if (AWAIT_ALL)\n {\n if (!mpe.workFunction)\n {\n mpe.workFunction = await fetchURI(mpe.codeLocation, this.supervisor.makeSafeOriginList('fetchWorkFunctions'));\n if(mpe.requirements.useStrict)\n mpe.useStrict = true;\n delete mpe.codeLocation;\n }\n if (!mpe.arguments)\n {\n let promises = [];\n let uris = mpe.argumentsLocation;\n if (uris)\n for (let i = 0; i < uris.length; i++)\n promises.push(fetchURI(uris[i].value, this.supervisor.makeSafeOriginList('fetchArguments')));\n\n mpe.arguments = await Promise.all(promises);\n\n // node localExec jobs read arguments from a file, so need to ensure they are properly parsed after being read.\n if (this.supervisor.options.localExec && !DCP_ENV.isBrowserPlatform)\n mpe.arguments = scopedKvin.parse(mpe.arguments[0]);\n\n delete mpe.argumentsLocation;\n }\n // if job input data is range object, we send the range object URI to the worker\n if (!mpe.mro && mpe.MROLocation)\n {\n mpe.mro = await fetchURI(mpe.MROLocation, this.supervisor.makeSafeOriginList('fetchData'));\n delete mpe.MROLocation;\n }\n }\n else\n {\n let promises = [];\n\n // Get workFn.\n if (!mpe.workFunction)\n {\n const workFunctionPromise = fetchURI(mpe.codeLocation, this.supervisor.makeSafeOriginList('fetchWorkFunctions'))\n .then((workFunction) => {\n mpe.workFunction = workFunction;\n if (mpe.requirements.useStrict)\n mpe.useStrict = true;\n delete mpe.codeLocation;\n });\n promises.push(workFunctionPromise);\n }\n\n // if job input data is range object, we send the range object URI to the worker\n if (!mpe.mro && mpe.MROLocation)\n {\n const mroPromise = fetchURI(mpe.MROLocation, this.supervisor.makeSafeOriginList('fetchData'))\n .then((mro) => {\n mpe.mro = mro;\n delete mpe.MROLocation;\n });\n promises.push(mroPromise)\n }\n\n // Get workFn args.\n if (!mpe.arguments && mpe.argumentsLocation)\n {\n mpe.arguments = new Array(mpe.argumentsLocation.length);\n for (let k = 0; k < mpe.argumentsLocation.length; k++)\n promises.push(fetchURI(mpe.argumentsLocation[k].value, this.supervisor.makeSafeOriginList('fetchArguments'))\n .then((arg) => (mpe.arguments[k] = arg) ));\n }\n\n await Promise.all(promises);\n\n if (mpe.argumentsLocation) delete mpe.argumentsLocation;\n }\n\n return mpe;\n }\n\n /**\n * Look up slice.datumUri or use the range object this.mro, constructed by fetchJob.\n * @param {Slice} slice \n * @returns {Promise<{ inputDatum, dataError }>}\n */\n async fetchSliceData (datumUri, slice) // eslint-disable-line require-await\n {\n const that = this;\n function catchHandler(error)\n {\n const dataError = error;\n dataError['errorCode'] = (error.code === 'EFETCH') ? 'EFETCH' : 'EUNCAUGHTERROR';\n that.emit('workEmit', {\n eventName: 'error',\n payload: {\n message: dataError.message,\n stack: dataError.stack,\n name: that.public.name\n }\n });\n return dataError;\n }\n\n if (!datumUri && !this.state.is(READY))\n throw new Error(`When !datumUri, JobManager '${this.identifier}' must be in READY state.`);\n\n try\n {\n if (!datumUri)\n {\n if (!this.mro) throw new Error('Must complete call to JobManager.fetchJob before calling JobManager.fetchSliceData.');\n const ro = rehydrateRange(this.mro);\n // Slice numbers start at 1.\n const inputDatum = ro[slice.sliceNumber - 1];\n debugging('jobmanager') && console.debug(`Fetched datum: ${stringify(inputDatum, 512)}`);\n return { inputDatum, dataError: null };\n }\n\n return fetchURI(datumUri, this.supervisor.makeSafeOriginList('fetchData'))\n .then((inputDatum) => {\n debugging('jobmanager') && console.debug(`Fetched datum: ${stringify(inputDatum, 512)}`);\n return { inputDatum, dataError: null };\n })\n .catch((error) => {\n const dataError = catchHandler(error);\n return { inputDatum: null, dataError };\n });\n }\n catch (error)\n {\n const dataError = catchHandler(error);\n return { inputDatum: null, dataError };\n }\n }\n\n // _Idx\n //\n // Miscellaneous.\n //\n\n /**\n * Debugging helper.\n * @param {string} tag\n */\n dumpSlices(tag='')\n {\n if (this.sliceInventory.length < 1) return;\n const queued = [], working = [], finished = [];\n for (const slice of this.sliceInventory)\n {\n if (slice.isUnassigned || slice.isReady)\n queued.push(slice);\n else if (slice.isReserved || slice.isWorking)\n working.push(slice);\n else\n finished.push(slice);\n }\n console.log(`${tag}:--JobManager.dumpSlices--${this.identifier} slices ${this.sliceInventory.length}-------`);\n console.log(`-----queued(${queued.length})-----------------------------------------------------------------`);\n for (const slice of queued)\n console.log(slice.identifier);\n console.log(`-----working(${working.length})----------------------------------------------------------------`);\n for (const slice of working)\n console.log(slice.identifier);\n console.log(`-----finished(${finished.length})---------------------------------------------------------------`);\n for (const slice of finished)\n console.log(slice.identifier);\n console.log('-----------------------------------------------------------------------------------');\n }\n\n /**\n * Calls removeStackTrace when !this.displayMaxDiagInfo .\n * @param {string|Error} error\n * @returns {string|Error}\n */\n checkStackTrace(error)\n {\n return this.displayMaxDiagInfo ? error : this.supervisor.removeStackTrace(error);\n }\n\n /**\n * Add slices to the job manager's inventory.\n *\n * @param {SliceMessage[]} sliceMessages - Messages from task distributor describing slices.\n * @param {object} authorizationMessage - The signature that shipped with the task authorizing this worker.\n */\n addSlices (sliceMessages, authorizationMessage)\n {\n sliceMessages.forEach((sliceMessage) => {\n const slice = new Slice(this, sliceMessage, authorizationMessage);\n if (!slice.isEstimation) this.isEstimation = false;\n this.sliceInventory.push(slice);\n });\n }\n\n /**\n * Schedule the slice to be executed.\n * @param {Slice} slice\n * @param {boolean} [placeInTheFrontOfTheQueue=false]\n */\n scheduleSlice(slice, placeInTheFrontOfTheQueue = false)\n {\n // Reset slice state to allow execution.\n slice.resetState();\n // Enqueue in the to-be-executed queue.\n if (placeInTheFrontOfTheQueue) this.sliceInventory.unshift(slice);\n else this.sliceInventory.push(slice);\n }\n\n /**\n * XXXpfr @todo UNUSED but may be used soon\n * Returns a counterfeit JobManager - a generic object with most of the same\n * ownProperties as a real exports.JobManager. Any inventories and synchronizer are\n * duplicated (equivalent state, no events copied, ~ shallow clone)\n * @returns {JobManager}\n */\n counterfeit()\n {\n /** @type {JobManager} */\n const fake = {};\n \n for (let prop in Object.keys(this))\n {\n if (this[prop] instanceof Inventory)\n fake[prop] = this[prop].duplicate();\n else if (this[prop] instanceof Synchronizer)\n fake[prop] = this[prop].valueOf();\n else\n fake[prop] = this[prop];\n }\n\n return fake;\n }\n\n}\nexports.JobManager = JobManager;\n\n//# sourceURL=webpack://dcp/./src/dcp-client/worker/supervisor2/job-manager.js?");
4538
+
4539
+ /***/ }),
4540
+
4541
+ /***/ "./src/dcp-client/worker/supervisor2/load.js":
4542
+ /*!***************************************************!*\
4543
+ !*** ./src/dcp-client/worker/supervisor2/load.js ***!
4544
+ \***************************************************/
4545
+ /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
4546
+
4547
+ eval("/**\n * @file worker/supervisor2/load.js\n *\n * A support class for Supervisor2 for cpu/gpu load densities.\n *\n * @author Wes Garland, wes@kingsds.network,\n * Paul, paul@kingsds.network,\n * @date Dec 2020,\n * June 2022,\n */\n\n const { assert } = __webpack_require__(/*! dcp/common/dcp-assert */ \"./src/common/dcp-assert.js\");\n\nclass Load\n{\n constructor (cpu, gpu)\n {\n if (arguments[0] && typeof arguments[0] === 'object')\n ({cpu, gpu} = arguments[0]);\n \n this.cpu = cpu ? cpu : 1.0;\n this.gpu = gpu ? gpu : 0.0;\n \n assert(typeof this.cpu === 'number');\n assert(typeof this.gpu === 'number');\n }\n \n add (cpu, gpu)\n {\n if (arguments[0] && typeof arguments[0] === 'object')\n ({cpu, gpu} = arguments[0]);\n \n this.cpu += cpu;\n this.gpu += gpu;\n \n assert(typeof this.cpu === 'number');\n assert(typeof this.gpu === 'number');\n \n return this;\n }\n \n subtract (cpu, gpu)\n {\n if (arguments[0] && typeof arguments[0] === 'object')\n ({cpu, gpu} = arguments[0]);\n \n this.cpu -= cpu;\n this.gpu -= gpu;\n \n assert(typeof this.cpu === 'number');\n assert(typeof this.gpu === 'number');\n \n return this;\n }\n \n scale (cpuScalar, gpuScalar)\n {\n if (arguments[0] && typeof arguments[0] === 'object')\n ({cpuScalar, gpuScalar} = arguments[0]);\n \n this.cpu *= cpuScalar;\n this.gpu *= gpuScalar;\n \n assert(typeof this.cpu === 'number');\n assert(typeof this.gpu === 'number');\n \n return this;\n }\n \n lessThan (cpu, gpu)\n {\n if (arguments[0] && typeof arguments[0] === 'object')\n ({cpu, gpu} = arguments[0]);\n \n assert(typeof cpu === 'number');\n assert(typeof gpu === 'number');\n \n return this.cpu <= cpu && this.gpu <= gpu;\n }\n \n fits (limit, cpu, gpu)\n {\n if (arguments[1] && typeof arguments[1] === 'object')\n ({cpu, gpu} = arguments[1]);\n \n if (limit.cpu - (this.cpu + cpu) < 0)\n return false;\n \n if (limit.gpu - (this.gpu + gpu) < 0)\n return false;\n \n return true;\n }\n}\nexports.Load = Load;\n\n\n//# sourceURL=webpack://dcp/./src/dcp-client/worker/supervisor2/load.js?");
4548
+
4549
+ /***/ }),
4550
+
4551
+ /***/ "./src/dcp-client/worker/supervisor2/module-cache.js":
4552
+ /*!***********************************************************!*\
4553
+ !*** ./src/dcp-client/worker/supervisor2/module-cache.js ***!
4554
+ \***********************************************************/
4555
+ /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
4556
+
4557
+ eval("/**\n * @file worker/module-cache.js\n *\n * A cache for modules.\n *\n * @author Matthew Palma, mpalma@kingsds.network\n * Paul, paul@kingsds.network\n * @date May 2019\n * June 2022\n */\n\nconst debugging = (__webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope)('worker');\nconst { EventEmitter } = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\");\n \nclass ModuleCache extends EventEmitter\n{\n constructor (supervisor)\n {\n super('ModuleCache')\n this.supervisor = supervisor;\n this.cache = {}; // Modules keyed by module group.\n }\n\n /**\n * Returns an object listing what jobs and modules are currently cached.\n * @returns {string} - in the form: moduleGroup1: [ job1 ], moduleGroup2: [ job1, job2 ]...\n */\n get cacheDescription ()\n {\n const desc = {};\n for (const [ id, moduleJobs ] of Object.entries(this.cache))\n desc[id] = moduleJobs.jobs;\n return JSON.stringify(desc);\n }\n\n /**\n * Attempts to look up an item from the cache.\n * @param {string} id - the items module group name\n * @param {string} job - the job calling fetch\n * @returns {any} The value of the stored item or undefined if nothing is found\n */\n fetch(id, job)\n {\n const moduleJobs = this.cache[id];\n if (!moduleJobs) return moduleJobs;\n if (moduleJobs.jobs.indexOf(job) === -1 )\n moduleJobs.jobs.push(job);\n return moduleJobs.module;\n }\n\n /**\n * Stores a fetched value for one of the caches.\n * @param {string} id - the items identifier module group name\n * @param {string} job - the job calling fetch\n * @param {any} value - the item to store\n * @returns {any} - @value passed in\n */\n store (id, job, value)\n {\n if (!this.cache[id]) this.cache[id] = { module: value, jobs: [ job ] };\n else if (this.cache[id].jobs.indexOf(job) === -1) this.cache[id].jobs.push(job);\n return value\n }\n \n /**\n * Removes a job reference from a module cache.\n * When there are no jobs left, the module cache entry is destroyed.\n * @param {string} job - the job to be removed.\n */\n removeJob (job)\n {\n for (const [ id, moduleJobs ] of Object.entries(this.cache))\n {\n moduleJobs.jobs = moduleJobs.jobs.filter(_job => _job !== job);\n if (moduleJobs.jobs.length < 1) delete this.cache[id];\n }\n }\n\n /**\n * Attempts to fetch a module group from the cache and\n * if it's not found it attempts to fetch then store\n * the module group from the package manager.\n *\n * @param {array} modulesArray - the array of modules requested \n * - (when stringified it's the identifier of the module group)\n *\n * @returns {Promise<object>} - the module group\n * @throws when the module group can not be fetched\n */\n async fetchModule(modulesArray, job)\n {\n const cacheKey = JSON.stringify(modulesArray);\n let modules = this.fetch(cacheKey, job);\n if (modules !== null)\n {\n debugging() && console.debug('FetchModule: Cache hit', { cacheKey, job });\n return modules;\n }\n\n const {\n success,\n payload: responsePayload,\n } = await this.supervisor.packageManagerConnection.send('fetchModule', {\n modules: modulesArray,\n });\n\n if (!success)\n {\n /**\n * Preserving the error message by not rewrapping it with DCPError in case\n * we want to let clients know which module couldn't be fetched.\n */\n throw responsePayload;\n }\n\n modules = await responsePayload;\n return this.store(cacheKey, modules, job);\n }\n}\nexports.ModuleCache = ModuleCache;\n\n\n//# sourceURL=webpack://dcp/./src/dcp-client/worker/supervisor2/module-cache.js?");
4558
+
4559
+ /***/ }),
4560
+
4561
+ /***/ "./src/dcp-client/worker/supervisor2/rolling-statistics.js":
4562
+ /*!*****************************************************************!*\
4563
+ !*** ./src/dcp-client/worker/supervisor2/rolling-statistics.js ***!
4564
+ \*****************************************************************/
4565
+ /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
4566
+
4567
+ eval("/**\n * @file worker/supervisor2/rolling-statistics.js\n *\n * A support class for Supervisor2 for the statistics of load densities.\n *\n * @author Paul, paul@kingsds.network,\n * @date June 2022,\n */\n\n//const { assert } = require('dcp/common/dcp-assert');\nconst assert = __webpack_require__(/*! assert */ \"./node_modules/assert/build/assert.js\");\n \nclass Stddev\n{\n constructor()\n {\n this.count = 0;\n this.mean = 0;\n this.variance = 0;\n this.x = [];\n }\n get stddev () { return Math.sqrt(this.variance); }\n check()\n {\n assert(typeof this.count === 'number' && this.count >= 0);\n assert(typeof this.mean === 'number');\n assert(typeof this.variance === 'number');\n }\n add(value)\n {\n assert(typeof value === 'number');\n \n this.x.push(value);\n const lastMean = this.mean;\n this.mean = this.computeMean(value);\n this.variance = this.computeVariance(value, lastMean)\n this.count++;\n this.check();\n \n return this;\n }\n computeMean(value)\n {\n return (value + this.count * this.mean) / (this.count + 1);\n }\n computeVariance(value, lastMean)\n {\n if (this.count < 1) return 0;\n // n = this.count + 1\n // ss2(n) = x1^2 + ... + x(n-1)^2 + xn^2 = xn^2 + ss2(n-1)\n // variance(n) = ( (x1-mean(n))^2 + ... + (xn-mean(n))^2 ) / (n-1)\n // V(n) = ((n-1)/n)*variance(n) = ss2(n) / n - mean(n)^2\n // (n-1)*variance(n) = n*V(n) = ss2(n) - n*mean(n)^2\n // = xn^2 + ss2(n-1) - n*mean(n)^2\n // = xn^2 + (n-1)*V(n-1) + (n-1)*mean(n-1)^2 - n*mean(n)^2\n // = xn^2 + (n-2)*variance(n-1) + (n-1)*mean(n-1)^2 - n*mean(n)^2\n // variance(n) = (xn^2 + (n-2)*variance(n-1) + (n-1)*mean(n-1)^2 - n*mean(n)^2) / (n-1)\n return (value * value + (this.count - 1) * this.variance + this.count * lastMean * lastMean - (this.count + 1) * this.mean * this.mean) / this.count;\n }\n}\nexports.Stddev = Stddev;\nclass EMA\n{\n constructor(alpha)\n {\n this.count = 0;\n this.alpha = alpha; // period = 2/this.alpha - 1, when alpha=0.5, period=3\n this.ema = 0;\n }\n\n check()\n {\n assert(typeof this.count === 'number' && this.count >= 0);\n assert(typeof this.alpha === 'number' && this.alpha > 0 && this.alpha < 1);\n assert(typeof this.ema === 'number');\n }\n add(value)\n {\n assert(typeof value === 'number');\n \n this.ema = this.computeEMA(value);\n this.count++;\n this.check();\n \n return this;\n }\n computeEMA(value)\n {\n if (this.count < 1)\n return value;\n return this.alpha * value + (1 - this.alpha) * this.ema;\n }\n}\nexports.EMA = EMA;\nclass MA\n{\n constructor(period)\n {\n this.period = period;\n this.count = 0;\n this.ma = 0;\n this.x = [];\n }\n check()\n {\n assert(typeof this.count === 'number' && this.count >= 0);\n assert(typeof this.period === 'number' && this.period > 1);\n assert(typeof this.ma === 'number');\n }\n add(value)\n {\n assert(typeof value === 'number');\n \n this.x.push(value);\n this.ma = this.computeMA(value);\n this.count++;\n this.check();\n \n return this;\n }\n computeMA(value)\n {\n if (this.count < 1)\n return value;\n if (this.count < this.period)\n return (value + this.count * this.ma) / (this.count + 1);\n return (value + this.period * this.ma - this.x[this.count - this.period]) / this.period;\n }\n}\nexports.MA = MA;\n//--------------------------------------------------------------------------------------------\n// Combos -- wish we could do mixins.\n//--------------------------------------------------------------------------------------------\nclass EMAAndStddev extends Stddev\n{\n constructor(alpha)\n {\n super();\n this.alpha = alpha; // period = 2/this.alpha - 1, when alpha=0.5, period=3\n this.ema = 0;\n }\n check()\n {\n super.check();\n assert(typeof this.alpha === 'number' && this.alpha > 0 && this.alpha < 1);\n assert(typeof this.ema === 'number');\n }\n add(value)\n {\n assert(typeof value === 'number');\n\n this.ema = this.computeEMA(value);\n super.add(value);\n\n this.check();\n\n return this;\n }\n computeEMA(value)\n {\n if (this.count < 1)\n return value;\n return this.alpha * value + (1 - this.alpha) * this.ema;\n }\n}\nexports.EMAAndStddev = EMAAndStddev;\nclass Statistics extends EMAAndStddev\n{\n constructor(alpha)\n {\n super(alpha)\n this.ma = 0;\n }\n\n get period () { return 2 / this.alpha - 1; }\n\n check()\n {\n super.check();\n assert(typeof this.period === 'number' && this.period > 1);\n assert(typeof this.ma === 'number');\n }\n add(value)\n {\n assert(typeof value === 'number');\n\n this.ma = this.computeMA(value);\n super.add(value);\n\n this.check();\n\n return this;\n }\n computeMA(value)\n {\n if (this.count < 1)\n return value;\n if (this.count <= this.period)\n return (value + this.count * this.ma) / (this.count + 1);\n return (value + this.period * this.ma - this.x[this.count - this.period]) / this.period;\n }\n}\nexports.Statistics = Statistics;\n\n\n//# sourceURL=webpack://dcp/./src/dcp-client/worker/supervisor2/rolling-statistics.js?");
4568
+
4569
+ /***/ }),
4570
+
4571
+ /***/ "./src/dcp-client/worker/supervisor2/sandbox2.js":
4572
+ /*!*******************************************************!*\
4573
+ !*** ./src/dcp-client/worker/supervisor2/sandbox2.js ***!
4574
+ \*******************************************************/
4575
+ /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
4576
+
4577
+ "use strict";
4578
+ eval("/* eslint-disable require-await */\n// NOTE - need timeout/postmessage function\n/**\n * @file dcp-client/worker/supervisor2/sandbox.js\n *\n * A sandbox that when constructed and assigned can do work for\n * a distributed slice. A sandbox runs for a single slice at a time.\n *\n * Usage (simplified...):\n * const sandbox = new Sandbox(this.cache, { ...this.options.sandboxOptions }, this.allowedOrigins);\n * await sandbox.start(delayStartMs);\n * await sandbox.assign(this, delayAssignMs);\n * return sandbox.work(slice, delayWorkMs)\n * .then((result) => { \n * slice.collectResult(result, true);\n * sandbox.changeWorkingToAssigned();\n * this.supervisor.recordResult(slice)\n * })\n * .catch((error) => {\n * slice.collectResult(error, false);\n * const reason = this.supervisor.handleSandboxWorkError(sandbox, slice, error);\n * this.supervisor.returnSlice(slice, reason);\n * this.returnSandbox(sandbox);\n * });\n *\n * Debug flags:\n * Sandbox.debugWork = true // - turns off 30 second timeout to let user debug sandbox innards more easily\n * Sandbox.debugState = true // - logs all state transitions for this sandbox\n * Sandbox.debugEvents = true // - logs all events received from the sandbox\n *\n * @author Matthew Palma, mpalma@kingsds.network\n * Ryan Rossiter, ryan@kingsds.network\n * Wes Garland, wes@kingsds.network\n * Paul, paul@kingsds.network\n * @date May 2019\n * May 2019\n * Decemeber 2020\n * June 2022\n * @module sandbox\n */\n\n/* global dcpConfig */ // eslint-disable-line no-redeclare\n// @ts-check\n\n\nconst debugging = (__webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope)('worker');\nconst { assert, assertEq3 } = __webpack_require__(/*! dcp/common/dcp-assert */ \"./src/common/dcp-assert.js\");\nconst DCP_ENV = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\nconst { Synchronizer } = __webpack_require__(/*! dcp/common/concurrency */ \"./src/common/concurrency.js\");\nconst nanoid = (__webpack_require__(/*! nanoid */ \"./node_modules/nanoid/index.browser.js\").nanoid);\nconst { EventEmitter } = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\");\nconst { truncateAddress } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\nconst kvin = __webpack_require__(/*! kvin */ \"./node_modules/kvin/kvin.js\");\nconst scopedKvin = new kvin.KVIN({Object: ({}).constructor,\n Array: ([]).constructor, \n Function: (()=>{}).constructor});\n\nlet timeDilation = 1;\nif (DCP_ENV.platform === 'nodejs') {\n const { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\n /** Make timers 10x slower when running in niim */\n timeDilation = (requireNative('module')._cache.niim instanceof requireNative('module').Module) ? 10 : 1;\n}\nconst addressTruncationLength = 20;\nconst workerTuning = dcpConfig.worker;\ntimeDilation = 1;\nlet sbCnter = 0; // Global counter of terminated sandboxes guarded by debugging().\n\n/**\n * Wraps console.debug to emulate debug module prefixing messages on npm.\n * @param {...any} args\n */\nconst debug = (...args) => {\n if (debugging()) {\n console.debug('Sandbox:', ...args);\n }\n};\n\n// Sandbox states\nconst UNREADY = 'UNREADY' // No Sandbox (web worker, saworker, etc) has been constructed yet\nconst READYING = 'READYING' // Sandbox is being constructed and environment (bravojs, env) is being set up\nconst READY_FOR_ASSIGN = 'READY_FOR_ASSIGN' // Sandbox is ready to be assigned\nconst ASSIGNED = 'ASSIGNED' // Sandbox is assigned but not working\nconst ASSIGNING = 'ASSIGNING' // Sandbox is in the process of being ASSIGNED\nconst WORKING = 'WORKING' // Sandbox is working\nconst BROKEN = 'BROKEN' // Sandbox is broken and should be terminated.\nconst EVAL_RESULT_PREFIX = 'evalResult::';\n\nclass SandboxError extends Error { constructor(errorCode, ...args) { super(...args); this.errorCode = errorCode; }}\nclass NoProgressError extends SandboxError { constructor(...args) { super('ENOPROGRESS', ...args); } }\nclass SliceTooSlowError extends SandboxError { constructor(...args) { super('ESLICETOOSLOW', ...args); } }\nclass UncaughtExceptionError extends SandboxError { constructor(...args) { super('EUNCAUGHT', ...args); } }\nclass RemoteFetchError extends SandboxError { constructor(...args) { super('EFETCH', ...args); } }\n\nfunction fillInError(errorCtor, errorIn) {\n const errorOut = new errorCtor(errorIn.message);\n errorOut.name = errorIn.name;\n errorOut.fileName = errorIn.fileName;\n errorOut.lineNumber = errorIn.lineNumber;\n errorOut.stack = errorIn.stack;\n return errorOut;\n}\n\n/** @typedef {import('dcp/common/dcp-events').EventEmitter} EventEmitter */\n/** @typedef {import('./slice2').Slice} Slice */\n/** @typedef {import('./job-manager').JobManager} JobManager */\n/** @typedef {import('./module-cache').ModuleCache} ModuleCache */\n\n/**\n * @access public\n * @typedef {object} SandboxOptions\n * @constructor {function} [SandboxConstructor]\n * @property {boolean} [ignoreNoProgress] - When true, the sandbox will not be stopped for not calling progress\n */\n\nclass Sandbox extends EventEmitter {\n /**\n * A Sandbox (i.e. a worker sandbox) which executes distributed slices.\n *\n * @constructor\n * @param {ModuleCache} moduleCache\n * @param {SandboxOptions} options\n * @param {string[]} origins\n */\n constructor (moduleCache, options, origins) {\n super('Sandbox');\n /** @type {ModuleCache} */\n this.moduleCache = moduleCache;\n /** @type {SandboxOptions} */\n this.options = {\n ignoreNoProgress: false,\n ...options,\n SandboxConstructor: options.SandboxConstructor || (__webpack_require__(/*! ../evaluators */ \"./src/dcp-client/worker/evaluators/index.js\").BrowserEvaluator),\n }\n /** @type {string[]} */\n this.allowedOrigins = origins;\n /** @type {Synchronizer} */\n this.state = new Synchronizer(UNREADY, [ UNREADY, READYING, READY_FOR_ASSIGN, ASSIGNING, ASSIGNED, WORKING, BROKEN ]);\n\n /** @type {string} */\n this.jobAddress = null;\n /** @type {object} */\n this.evaluatorHandle = null;\n /** @type {object} */\n this.capabilities = null;\n /** @type {EventEmitter} */\n this.ee = new EventEmitter('SandboxInternal');\n\n /** @type {boolean} */\n this.terminated = false;\n /** @type {number?} */\n this.progress = 100;\n /** @type {object} */\n this.progressReports = null;\n /** @type {object} */\n this.progressTimeout = null;\n /** @type {object} */\n this.sliceTimeout = null;\n /** @type {object} */\n this.rejectionData = null;\n /** @type {Slice} */\n this.slice = null;\n /** @type {number?} */\n this.sliceStartTime = null;\n /** @type {number} */\n this.id = Sandbox.getNewId();\n\n this.ringMessageHandlers = [\n this.handleRing0Message,\n this.handleRing1Message,\n this.handleRing2Message,\n this.handleRing3Message,\n ];\n\n this.resetSliceTimeReport();\n }\n\n /** @type {string} - debug string that characterizes sandbox. */\n get identifier() {\n if (!this.jobAddress) return `${this.id}`;\n return `${this.id}.${truncateAddress(this.jobAddress, addressTruncationLength)}.${this.state?this.state:'<null>'}`;\n }\n\n static getNewId() {\n return Sandbox.idCounter++;\n }\n\n /** @type {boolean} */\n get isReadyForAssign () {\n return this.state.is(READY_FOR_ASSIGN);\n }\n /** @type {boolean} */\n get isAssigned () {\n return this.state.is(ASSIGNED);\n }\n /** @type {boolean} */\n get isWorking () {\n return this.state.is(WORKING);\n }\n /** @type {boolean} */\n get isBroken () {\n return this.state.is(BROKEN);\n }\n /** @type {boolean} */\n get isTerminated () {\n return this.terminated;\n }\n\n /**\n * Mark WORKING sandbox as ASSIGNED in preparation for possible reuse,\n */\n changeWorkingToAssigned () {\n this.state.testAndSet(WORKING, ASSIGNED);\n }\n \n /**\n * Remove from collection of ASSIGNED sandboxes in preparation for termination,\n */\n markAsUnready() {\n this.state.testAndSet(ASSIGNED, UNREADY);\n }\n \n /**\n * Transitions: ASSIGNED --> WORKING and assigns the slice.\n * @param {Slice} slice \n */\n markAsWorking (slice) {\n if (!this.isAssigned)\n throw new Error(`Sandbox ${this.identifier} is not ready to work`);\n this.state.set(ASSIGNED, WORKING);\n this.slice = slice;\n }\n \n /**\n * Fancy assert.\n */\n verifyWorking () {\n if (!this.isWorking) {\n throw new Error(`Sandbox ${this.identifier} is not working`);\n }\n }\n\n /**\n * Readies the sandbox. This will result in the sandbox being ready and not assigned,\n * it will need to be assigned with a job before it is able to do work.\n *\n * @todo maybe preload specific modules or let the cache pass in what modules to load?\n * @param {number} [delay=0]\n * @returns {Promise<Sandbox>}\n * @throws on failure to ready\n */\n async start(delay = 0) {\n this.state.set(UNREADY, READYING);\n\n if (delay > 0) await new Promise((resolve) => setTimeout(resolve, delay * timeDilation));\n\n try {\n // RING 0\n this.evaluatorHandle = new this.options.SandboxConstructor({\n name: `DCP Sandbox #${this.id}`,\n });\n this.evaluatorHandle.onerror = this.onerror.bind(this);\n\n const messageHandler = this.onmessage.bind(this);\n this.evaluatorHandle.onmessage = function onmessage(event)\n {\n let data;\n if (event.data.serialized)\n data = kvin.parse(event.data.message);\n else\n data = kvin.unmarshal(event.data);\n messageHandler({ data });\n }\n\n const evaluatorPostMessage = this.evaluatorHandle.postMessage.bind(this.evaluatorHandle);\n this.evaluatorHandle.postMessage = function postMessage(message)\n {\n evaluatorPostMessage(scopedKvin.marshal(message));\n }\n\n const that = this;\n this.evaluatorHandle.addEventListener('end', () => that.terminate(true));\n\n // Now in RING 1\n\n // Now in RING 2\n await this.describe();\n this.state.set(READYING, READY_FOR_ASSIGN);\n this.emit('ready', this);\n } catch (error) {\n console.warn('Failed to start the sandbox -', error.message);\n this.state.set(READYING, BROKEN);\n this.terminate(false);\n throw error;\n }\n \n return this;\n }\n\n /**\n * This will assign the sandbox with a job, loading its sandbox code\n * into the sandbox.\n *\n * @param {JobManager} jobManager - The job manager that will be the owner of this sandbox.\n * @param {number} delay\n * @returns {Promise<Sandbox>}\n * @throws on initialization failure\n */\n async assign (jobManager, delay = 0) {\n \n if (delay > 0) await new Promise((resolve) => setTimeout(resolve, delay * timeDilation));\n \n this.state.set(READY_FOR_ASSIGN, ASSIGNING);\n this.jobAddress = jobManager.address;\n this.job = jobManager.jobMessage;\n \n assertEq3(this.job.address, this.jobAddress);\n assert(typeof this.job === 'object');\n assert(typeof this.job.requirements === 'object');\n assert(Array.isArray(this.job.dependencies));\n assert(Array.isArray(this.job.requirePath));\n\n // Extract public data from job, with defaults\n this.public = Object.assign({\n name: `Anonymous Job ${this.job.address.slice(0, 6)}`,\n description: 'Discreetly helping make the world smarter.',\n link: 'https://distributed.computer/about',\n }, this.job.public);\n\n // Future: We may want other filename tags for appliances // RR Nov 2019\n\n // Important: The order of applying requirements before loading the sandbox code\n // is important for modules and sandbox code to set globals over the whitelist.\n await this.applySandboxRequirements(this.job.requirements);\n await this.assignEvaluator();\n \n return this;\n }\n \n async assignEvaluator() {\n debug('Begin assigning job to evaluator');\n const that = this;\n\n return new Promise(function sandbox$$assignEvaluatorPromise(resolve, reject) {\n const message = {\n request: 'assign',\n job: that.job,\n sandboxConfig: workerTuning.sandbox,\n };\n\n /* note - onFailListener used for removal. This is necessary due to a bug in ee.once. /wg Feb 2022 */\n \n const onSuccess = (event) => {\n // eslint-disable-next-line no-use-before-define\n that.ee.removeListener('reject', onFailListener);\n that.emit('assigned', event.jobAddress);\n debug('Job assigned to evaluator');\n resolve();\n };\n\n const onFail = (error) => {\n // eslint-disable-next-line no-use-before-define\n that.ee.removeListener('assigned', onSuccessListener);\n reject(error);\n };\n\n const onSuccessListener = that.ee.once('assigned', onSuccess);\n const onFailListener = that.ee.once('reject', onFail);\n that.evaluatorHandle.postMessage(message);\n }).then((event) => {\n that.state.set(ASSIGNING, ASSIGNED);\n return event;\n }).catch(error => {\n that.state.set(ASSIGNING, BROKEN);\n debug('Failure in assigning job to evaluator', error);\n throw error;\n });\n }\n\n /**\n * Evaluates a string inside the sandbox.\n *\n * @param {string} code - the code to evaluate in the sandbox\n * @param {string} filename - the name of the 'file' to help with debugging,\n * no longer working though?\n * @returns {Promise} - resolves with eval result on success, rejects\n * otherwise\n */\n eval(code, filename) {\n const that = this;\n \n return new Promise(function sandbox$$eval$Promise(resolve, reject) {\n let msgId = nanoid();\n let msg = {\n request: 'eval',\n data: code,\n filename,\n msgId, \n };\n\n const eventId = EVAL_RESULT_PREFIX + msgId;\n\n const onSuccess = (event) => {\n // eslint-disable-next-line no-use-before-define\n that.ee.removeListener('reject', onFailListener);\n resolve(event);\n }\n\n const onFail = (error) => {\n // eslint-disable-next-line no-use-before-define\n that.ee.removeListener(eventId, onSuccessListener);\n reject(error);\n }\n\n const onSuccessListener = that.ee.once(eventId, onSuccess);\n const onFailListener = that.ee.once('reject', onFail);\n\n that.evaluatorHandle.postMessage(msg);\n })\n }\n\n /**\n * Resets the state of the bootstrap, without resetting the sandbox function if assigned.\n * Mostly used to reset the progress status before reusing a sandbox on another slice.\n * Must be called after @start.\n *\n * @returns {Promise} - resolves with result on success, rejects otherwise\n */\n resetSandboxState () {\n const that = this;\n\n return new Promise(function sandbox$resetSandboxStatePromise(resolve, reject) {\n let successCb, failTimeout;\n let msg = {\n request: 'resetState',\n };\n\n successCb = that.ee.once('resetStateDone', function sandbox$resetSandboxState$success () {\n if (failTimeout === false)\n return; /* already rejected */\n clearTimeout(failTimeout);\n failTimeout = false;\n resolve();\n });\n\n failTimeout = setTimeout(function sandbox$resetSandboxState$fail() {\n if (failTimeout === false)\n return; /* already resolved */\n \n that.ee.removeListener('resetStateDone', successCb);\n that.terminate(false);\n failTimeout = false;\n\n reject(new Error('resetState never received resetStateDone event from sandbox'));\n }, 3000 * timeDilation); /* XXXwg need tuneable */\n\n assert(that.evaluatorHandle); // It is possible that that.terminate nulls out evaluatorHandle before getting here.\n that.evaluatorHandle.postMessage(msg);\n });\n }\n\n /**\n * Clear all timers that are set inside the sandbox (evaluator) environment.\n *\n * @returns {Promise} - resolves with result on success, rejects otherwise\n */\n clearSandboxTimers() {\n const that = this;\n \n return new Promise(function sandbox$clearSandboxTimersPromise(resolve, reject) {\n let successCb, failTimeout;\n let msg = {\n request: 'clearTimers',\n };\n\n successCb = that.ee.once('clearTimersDone', function sandbox$clearSandboxTimers$success() {\n if (failTimeout === false)\n return; /* already rejected */\n clearTimeout(failTimeout);\n failTimeout = false;\n resolve();\n });\n\n failTimeout = setTimeout(function sanbox$clearSandboxTimers$fail() {\n if (failTimeout === false)\n return; /* already resolved */\n \n that.ee.removeListener('clearTimersDone', successCb);\n that.terminate(false);\n failTimeout = false;\n \n reject(new Error('clearTimers never received clearTimersDone event from sandbox'));\n }, 3000 * timeDilation); /* XXXwg need tuneable */\n\n if (that.evaluatorHandle) // Sometimes that.terminate nulls out evaluatorHandle before getting here.\n that.evaluatorHandle.postMessage(msg);\n });\n }\n\n /**\n * Sends a post message to describe its capabilities.\n *\n * Side effect: Sets the capabilities property of the current sandbox.\n *\n * @returns {Promise} Resolves with the sandbox's capabilities. Rejects with\n * an error saying a response was not received.\n * @memberof Sandbox\n */\n describe() {\n debug('Beginning to describe evaluator');\n const that = this;\n \n return new Promise(function sandbox$describePromise(resolve, reject) {\n if (that.evaluatorHandle === null) {\n return reject(new Error('Evaluator has not been initialized.'));\n }\n\n /**\n * Opted to create a flag for the describe response being received so that\n * we don't have to *hoist* the timeout's id to clear it in the response\n * handler.\n */\n let didReceiveDescribeResponse = false;\n const describeResponseHandler = that.ee.once('describe', (data) => {\n didReceiveDescribeResponse = true;\n const { capabilities } = data;\n if (typeof capabilities === 'undefined') {\n reject(new Error('Did not receive capabilities from describe response.'));\n }\n that.capabilities = capabilities;\n\n // Currently only used in tests. May use the event in the future.\n that.emit('described', capabilities);\n debug('Evaluator has been described');\n resolve(capabilities);\n });\n const describeResponseFailedHandler = () => {\n if (!didReceiveDescribeResponse) {\n that.ee.removeListener('describe', describeResponseHandler);\n that.terminate(false);\n reject(new Error( 'Describe message timed-out. No describe response was received from the describe command.'));\n }\n };\n\n const message = {\n request: 'describe',\n };\n\n // Arbitrarily set the waiting time.\n setTimeout(describeResponseFailedHandler, 6000 * timeDilation); /* XXXwg need tuneable */\n assert(that.evaluatorHandle); // It is possible that that.terminate nulls out evaluatorHandle before getting here.\n that.evaluatorHandle.postMessage(message);\n });\n }\n\n /**\n * Passes the job's requirements object into the sandbox so that the global\n * access lists can be updated accordingly.\n *\n * e.g. disallow access to OffscreenCanvas without\n * environment.offscreenCanvas=true present.\n *\n * Must be called after @start.\n *\n * @returns {Promise} - resolves with result on success, rejects otherwise\n */\n applySandboxRequirements(requirements) {\n const that = this;\n \n return new Promise(function sandbox$applySandboxRequirementsPromise(resolve, reject) {\n const message = {\n requirements,\n request: 'applyRequirements',\n };\n let wereRequirementsApplied = false;\n\n const successCb = that.ee.once(\n 'applyRequirementsDone',\n function sandbox$applyRequirements$success() {\n wereRequirementsApplied = true;\n resolve();\n },\n );\n\n assert(typeof message.requirements === 'object');\n that.evaluatorHandle.postMessage(message);\n\n setTimeout(function sandbox$finishApplySandboxRequirements() {\n if (!wereRequirementsApplied) {\n that.ee.removeListener('applyRequirementsDone', successCb);\n that.terminate(false);\n reject(new Error('applyRequirements never received applyRequirementsDone response from sandbox'));\n }\n }, 3000 * timeDilation); /* XXXwg needs tunable */\n });\n }\n\n /**\n * Executes a slice received from the supervisor.\n * Must be called after @start.\n *\n * @param {Slice} slice - bare minimum data required for the job/job code to be executed on\n * @param {number} [delay = 0] the delay that this method should wait before beginning work, used to avoid starting all sandboxes at once\n *\n * @returns {Promise} - resolves with result on success, rejects otherwise\n */\n\n async work (slice, delay = 0) {\n const that = this;\n\n assert(slice);\n\n // cf. DCP-1720\n this.resetSliceTimeReport();\n \n // Now wait for the delay if provided, prevents many sandboxes starting at once from crashing the supervisor\n if (delay > 0) await new Promise(resolve => setTimeout(resolve, (delay + 1) * timeDilation));\n if (!this.isWorking) return; // sandbox.terminate could have been called during the delay timeout\n\n // Prepare the sandbox to begin work\n // will be replaced by `assign` message that should be called before emitting a `work` message\n if (this.jobAddress !== slice.jobAddress) {\n throw new Error(`Sandbox.run: Sandbox is already assigned and jobAddress doesn't match previous (${this.jobAddress} !== ${slice.jobAddress})`);\n }\n\n let sliceHnd = { job: this.public, sandbox: this };\n await this.resetSandboxState();\n if (!this.slice) {\n console.error(`Slice for job ${this.jobAddress} vanished during work initialization - aborting`);\n return;\n }\n\n const { datum: inputDatum, error: dataError } = slice;\n\n this.resetProgressTimeout();\n this.resetSliceTimeout();\n\n return new Promise(function sandbox$$workPromise(resolve, reject) {\n let onSuccess, onFail\n\n onSuccess = that.ee.once('resolve', function sandbox$$work$success (event) {\n that.ee.removeListener('reject', onFail)\n resolve(event)\n }.bind(that));\n\n onFail = that.ee.once('reject', function sandbox$$work$fail (err) {\n that.ee.removeListener('resolve', onSuccess)\n reject(err)\n }.bind(that))\n\n that.sliceStartTime = Date.now();\n that.progress = null;\n that.progressReports = {\n last: undefined,\n lastDeterministic: undefined,\n };\n\n that.resetProgressTimeout();\n that.resetSliceTimeout();\n that.emit('start', sliceHnd);\n \n if(dataError){\n that.ee.removeListener('resolve', onSuccess);\n that.ee.removeListener('reject', onFail);\n setTimeout(() => reject(dataError), 0)\n\n } else {\n that.evaluatorHandle.postMessage({\n request: 'main',\n data: inputDatum,\n })\n }\n })\n .then(async function sandbox$$work$then(event) {\n // prevent any hanging timers from being fired\n await that.clearSandboxTimers();\n\n // TODO: Should sliceHnd just be replaced with that.public?\n that.emit('slice', sliceHnd); /** @todo: decide which event is right */\n that.emit('sliceFinish', event);\n that.emit('complete', that.jobAddress);\n\n that.state.set(WORKING, ASSIGNED);\n that.slice = false;\n\n return event;\n })\n .catch((err) => { \n if (err.name === 'EWORKREJECT') {\n that.rejectionData = err;\n that.evaluatorHandle.postMessage({ request: 'resetAndGetCPUTime' })\n } else { // sandbox termination for rejected work happens in Supervisor.handleRejectedWork\n // Ceci is the reject callback for when the slice throws an error\n that.terminate(false);\n }\n\n that.emit('error', err, 'slice');\n\n if (err instanceof NoProgressError) {\n that.emit('workEmit', {\n eventName: 'noProgress',\n payload: {\n timestamp: Date.now() - that.sliceStartTime,\n data: that.slice.datumUri,\n progressReports: that.progressReports,\n }\n });\n }\n throw err;\n })\n .finally(function sandbox$$work$finally() {\n that.emit('end', sliceHnd);\n });\n }\n\n resetProgressTimeout() {\n if (this.progressTimeout) {\n clearTimeout(this.progressTimeout);\n this.progressTimeout = null;\n }\n\n this.progressTimeout = setTimeout(() => {\n if (this.options.ignoreNoProgress) {\n return console.warn(\"ENOPROGRESS silenced by localExec: In a remote worker, this slice would be stopped for not calling progress frequently enough.\");\n }\n\n this.ee.emit('reject', new NoProgressError(`No progress event was received in the last ${dcpConfig.worker.sandbox.progressTimeout / 1000} seconds.`));\n }, +dcpConfig.worker.sandbox.progressTimeout * timeDilation);\n }\n\n resetSliceTimeout() {\n if (this.sliceTimeout) clearTimeout(this.sliceTimeout);\n\n this.sliceTimeout = setTimeout(() => {\n if (Sandbox.debugWork) return console.warn(\"Sandbox.debugWork: Ignoring slice timeout\");\n\n this.ee.emit('reject', new SliceTooSlowError(`Slice took longer than ${dcpConfig.worker.sandbox.sliceTimeout / 1000} seconds.`));\n }, +dcpConfig.worker.sandbox.sliceTimeout * timeDilation);\n }\n \n async handleRing0Message(data) {\n debugging('event:ring-0') && debug('event:ring-0', data);\n //handling a true ring 0 message\n switch (data.request) {\n case 'sandboxLoaded':\n // emit externally\n this.emit('sandboxLoaded', this)\n break;\n\n case 'scriptLoaded':\n // emit externally\n this.emit('scriptLoaded', data);\n if(data.result !== \"success\") {\n this.onerror(data);\n }\n break;\n \n case 'clearTimersDone':\n this.ee.emit(data.request, data);\n break;\n case 'totalCPUTime':\n this.updateTime(data);\n if (this.ee.listenerCount('resolve') > 0) {\n this.completeData.timeReport = this.sliceTimeReport;\n this.ee.emit('resolve', this.completeData);\n delete this.completeData;\n } else {\n this.rejectionData.timeReport = this.sliceTimeReport\n this.emit('rejectedWorkMetrics', this.rejectionData) // If there is no internal listener for 'resolve', the slice was rejected and\n delete this.rejectionData; // we need to send the rejected metrics to the supervisor\n } \n break;\n case 'error': {\n // Warning: rejecting here with just event.data.error causes issues\n // where the reject handlers modify the object so it interferes with the\n // workEmit event payload, wrapping in an Error instance copies the values\n const wrappedError = fillInError(UncaughtExceptionError, data.error);\n\n if (this.ee.listenerCount('reject') > 0) {\n this.ee.emit('reject', wrappedError);\n } else {\n // This will happen if the error is thrown during initialization\n throw e;\n }\n break;\n }\n default: {\n const errorMsg = new Error('Received unhandled request from sandbox: ' + data.request + '\\n\\t' + JSON.stringify(data));\n console.error(errorMsg);\n break;\n } \n }\n }\n\n async handleRing1Message(data) {\n switch (data.request) {\n case 'applyRequirementsDone':\n // emit internally\n this.ee.emit(data.request, data)\n break;\n default: {\n const errorMsg = new Error('Received unhandled request from sandbox ring 1: ' + data.request + '\\n\\t' + JSON.stringify(data));\n console.error(errorMsg)\n break; \n }\n }\n }\n\n async handleRing2Message(data) {\n debugging('event:ring-2') && debug('event:ring-2', data);\n switch (data.request) {\n case 'dependency': {\n let moduleData;\n try {\n moduleData = await this.moduleCache.fetchModule(data.data, this.jobAddress);\n } catch (error) {\n /*\n * In the event of an error here, we want to let the client know there was a problem in\n * loading their module. However, there hasn't yet been an actual slice assigned to the sandbox.\n * Therefore, we assign 'slice 0' to the sandbox, a slice that will never exist, and is used\n * purely for this purpose. \n */\n this.slice = {\n jobAddress: this.jobAddress,\n sliceNumber: 0,\n };\n\n const payload = {\n name: error.name,\n timestamp: error.timestamp,\n message: error.message,\n };\n\n const wrappedError = fillInError(RemoteFetchError, error);\n\n this.emit('workEmit', {\n eventName: 'error',\n payload,\n });\n this.ee.emit('reject', wrappedError);\n break;\n }\n this.evaluatorHandle.postMessage({\n request: 'moduleGroup',\n data: moduleData,\n id: data.id,\n });\n break;\n }\n case 'error':\n /*\n * Ring 2 error messages will only fire for problems inside of the worker that are separate from\n * the work function. In most cases there are other handlers for situations where 'error' may be emitted\n * such as timeouts if the expected message isn't recieved. Thus, we will output the error, but nothing else.\n */\n console.error(data.error);\n break;\n case 'describe':\n case 'evalResult':\n case 'resetStateDone':\n case 'assigned':\n // emit internally\n this.ee.emit(data.request, data);\n break;\n case 'reject':\n // emit internally\n this.ee.emit(data.request, data.error);\n break;\n default: {\n const error = new Error(`Received unhandled request from sandbox ring 2. Data: ${JSON.stringify(data, null, 2)}`);\n console.error(error);\n break;\n }\n }\n }\n\n async handleRing3Message(data) {\n switch (data.request) {\n case 'complete':\n clearTimeout(this.progressTimeout);\n clearTimeout(this.sliceTimeout);\n this.progressTimeout = this.sliceTimeout = null;\n\n if (this.progress === null) {\n if (this.options.ignoreNoProgress) {\n console.warn(\"ENOPROGRESS silenced by localExec: Progress was not called during this slice's execution, in a remote sandbox this would cause the slice to be failed\");\n } else {\n // If a progress update was never received (progress === null) then reject\n this.ee.emit('reject', new NoProgressError('Sandbox never emitted a progress event.'));\n break;\n }\n }\n this.evaluatorHandle.postMessage({ request: 'resetAndGetCPUTime' })\n this.progress = 100;\n this.completeData = data;\n // The timing report and resolve will be emitted when the CPU time is received. \n break;\n case 'progress': {\n let { progress, indeterminate, throttledReports, value } = data;\n this.progress = progress;\n const progressReport = {\n timestamp: Date.now() - this.sliceStartTime,\n progress,\n value,\n throttledReports,\n }\n this.progressReports.last = progressReport;\n if (!indeterminate) {\n this.progressReports.lastDeterministic = progressReport;\n }\n\n this.resetProgressTimeout();\n this.emit('sliceProgress', data);\n break;\n }\n case 'noProgress':\n this.ee.emit('reject', new NoProgressError(data.message));\n break;\n case 'console':\n data.payload.message = scopedKvin.marshal(data.payload.message);\n this.emit('workEmit', {\n eventName: 'console',\n payload: data.payload,\n });\n break;\n case 'emitEvent':/* ad-hoc event from the sandbox (work.emit) */\n this.emit('workEmit', {\n eventName: 'custom',\n payload: data.payload\n });\n break;\n case 'measurement':\n this.updateTime(data);\n break;\n case 'sandboxError': /* the sandbox itself has an error condition */\n this.emit('sandboxError', data.error);\n break;\n case 'workError': { /* the work function threw/rejected */\n this.emit('workEmit', {\n eventName: 'error',\n payload: data.error,\n });\n\n // Warning: rejecting here with just .data.error causes issues\n // where the reject handlers modify the object so it interferes with the\n // workEmit payload, wrapping in an Error instance copies the values\n const wrappedError = fillInError(UncaughtExceptionError, data.error);\n\n if (this.ee.listenerCount('reject') > 0) {\n this.ee.emit('reject', wrappedError);\n } else {\n // This will happen if the error is thrown during initialization\n throw wrappedError;\n }\n break;\n }\n default: {\n const errorMsg = new Error('Received unhandled request from sandbox ring 3: ' + data.request + '\\n\\t' + JSON.stringify(data));\n console.error(errorMsg)\n break; \n }\n }\n }\n\n /**\n * Handles progress and completion events from sandbox.\n * Unless explicitly returned out of this function will re-emit the event\n * on @this.ee where the name of the event is event.data.request.\n *\n * @param {object} event - event received from the sandbox\n */\n async onmessage(event) {\n debugging('event') && debug('event', event);\n if (Sandbox.debugEvents) {\n console.debug('sandbox - eventDebug:', {\n id: this.id,\n state: this.state ? this.state.valueOf() : '<undefined>',\n event: JSON.stringify(event)\n })\n }\n\n const { data } = event;\n const ringLevel = data.ringSource\n\n // Give the data to a handler depending on ring level\n if (ringLevel === -1) {\n console.error('Message sent directly from raw postMessage. Terminating worker...');\n console.debug(event);\n return this.terminate(true);\n } else {\n const handler = this.ringMessageHandlers[ringLevel];\n if (handler) {\n handler.call(this, data.value);\n } else {\n console.warn(`No handler defined for message from ring ${ringLevel}`);\n console.debug(event);\n }\n }\n }\n\n /**\n * Error handler for the internal sandbox.\n * Currently just logs the errors that the sandbox spits out.\n */\n onerror(event) {\n console.error('Sandbox emitted an error:', event);\n this.terminate(true, true);\n }\n\n /**\n * Clears the timeout and terminates the sandbox and sometimes emits a reject event.\n *\n * @param {boolean} [reject = true] - if true emit reject event\n * @param {boolean} [immediate = false] - passed to terminate, used by standaloneWorker to immediately close the connection\n */\n terminate (reject = true, immediate = false) {\n debugging() && console.debug(`Sandbox.terminate ${this.identifier}, count: ${++sbCnter}`);\n\n if (this.isTerminated) return;\n this.terminated = true;\n\n clearTimeout(this.progressTimeout);\n clearTimeout(this.sliceTimeout);\n this.progressTimeout = this.sliceTimeout = null;\n \n if (this.evaluatorHandle && typeof this.evaluatorHandle.terminate === 'function') {\n try {\n this.evaluatorHandle.terminate(immediate);\n this.evaluatorHandle = null;\n } catch (e) {\n console.error(`Error terminating sandbox ${this.id}:`, e);\n } finally {\n this.emit('terminate', this);\n }\n }\n\n if (reject) {\n this.ee.emit('reject', new Error(`Sandbox ${this.id} was terminated.`));\n }\n\n this.emit('terminated');\n }\n\n /**\n * Attempts to stop the sandbox from doing completing its current\n * set of work without terminating the working. \n * \n *** Until stop is implemented properly, use terminate(false).\n *\n * @todo Properly implement stop\n */\n stop () {\n //throw new Error('Sandbox.stop is not yet implemented.')\n this.terminate(false);\n }\n\n /**\n * ringNPostMessage can send a `measurement` request and update these\n * totals.\n */\n updateTime (measurementEvent) {\n ['total', 'CPU', 'webGL'].forEach((key) => {\n if (measurementEvent[key]) this.sliceTimeReport[key] += measurementEvent[key];\n })\n }\n\n resetSliceTimeReport () {\n this.sliceTimeReport = {\n total: 0,\n CPU: 0,\n webGL: 0,\n }\n }\n}\n\nSandbox.idCounter = 1;\nSandbox.debugWork = false;\nSandbox.debugState = false;\nSandbox.debugEvents = false;\n\nexports.Sandbox = Sandbox;\nexports.SandboxError = SandboxError;\nexports.NoProgressError = NoProgressError;\nexports.SliceTooSlowError = SliceTooSlowError;\nexports.UncaughtExceptionError = UncaughtExceptionError;\nexports.RemoteFetchError = RemoteFetchError;\n\n\n//# sourceURL=webpack://dcp/./src/dcp-client/worker/supervisor2/sandbox2.js?");
4579
+
4580
+ /***/ }),
4581
+
4582
+ /***/ "./src/dcp-client/worker/supervisor2/slice2.js":
4583
+ /*!*****************************************************!*\
4584
+ !*** ./src/dcp-client/worker/supervisor2/slice2.js ***!
4585
+ \*****************************************************/
4586
+ /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
4587
+
4588
+ "use strict";
4589
+ eval("/**\n * @file dcp-client/worker/supervisor2/slice2.js\n *\n * A wrapper for the slice object returned from the scheduler's task distributor.\n *\n * @author Matthew Palma, mpalma@kingsds.network\n * Ryan Rossiter, ryan@kingsds.network\n * Wes Garland, wes@kingsds.network\n * Paul, paul@kingsds.network\n * @date May 2019\n * May 2019\n * Decemeber 2020\n * June 2022\n * @module slice\n */\n\n// @ts-check\n\n\nconst { Synchronizer } = __webpack_require__(/*! dcp/common/concurrency */ \"./src/common/concurrency.js\");\nconst { assert } = __webpack_require__(/*! dcp/common/dcp-assert */ \"./src/common/dcp-assert.js\");\nconst debugging = (__webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope)('worker');\nconst { truncateAddress } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\n\nconst addressTruncationLength = 20;\n\nconst INITIAL = 'INITIAL';\nconst READY = 'READY';\nconst RESERVED = 'RESERVED';\nconst WORKING = 'WORKING';\nconst COMPLETE = 'COMPLETE';\nconst FAILED = 'FAILED';\nconst BROKEN = 'BROKEN';\nconst FINISHED = 'FINISHED';\n\n/** @typedef {import('./job-manager').JobManager} JobManager */\n/** @typedef {import('dcp/utils').SliceMessage} SliceMessage */\n/** @typedef {string} opaqueId */ // 22 character base64 string \n\n/** \n * Object use to represent a given slice inside the Supervisor. This object's shape \n * current inherits heavily from the message payload originating from the scheduler,\n * but should not /wg dec 2020.\n *\n * Caveat lector: this documentation was created without a 100% understanding of the\n * code. Please improve as possible.\n * \n * The read-only properties of this object are as follows:\n * - state INITIAL | READY | RESERVED | WORKING | COMPLETE | FAILED | BROKEN\n * - sliceNumber the number of the slice within this job\n * - jobAddress the address of the job that this slice belongs to\n * - isEstimation true when slice is used in estimation\n * - isLong true, when slice is estimated to take more than 300 seconds to execute\n * - sandbox the sandbox the slice will-be/is/has-been running on.\n * \n * - datum input set element for this slice this slice of the job; could be a data: URI or it could\n * be a URL we need to fetch; note that fetches are limited to worker's allow list\n * - result\n * - result.request 'complete',...\n * - result.result return value from work function\n * - result.timeReport { total, idle } ms \n * - error error info when slice FAILED\n * - jobManager wrapper for the job that owns this slice\n * - resultStorageType 'values' => we are storing individual values,\n * which could be data: URIs or URLs\n * at the scheduler\n * 'pattern' => user-specified pattern for result URLs.\n * Data will be uploaded via POST to the\n * URL matching the pattern, provided the\n * worker is allowed to access it.\n * - resultStorageParams user-supplied POST parameters sent to the result \n * storage server when using resultStorageType = pattern.\n * - resultStorageDetails the pattern when using resultStorageType = pattern.\n * - authorizationMessage authorization from task distributor, sent to result submitter, etc...\n * \n * - finished true, when in state COMPLETED, FAILED, BROKEN\n * - failed true, when in state FAILED, BROKEN\n * - completed true, when in state COMPLETED\n * - isUnassigned true, when in state INITIAL\n * - isReady true, when in state READY\n * - isWorking true, when in state WORKING\n * - identifier string 'sliceNumber.jobAddress.state'\n * - timeReport accessor for this.result.timeReport that updates from this.rejectedTimeReport when appropriate\n *\n * The r/w properties of this object are as follows:\n * - etaMs estimate of slice completion time span\n * - rejected slice has been rejected\n * - rejectedTimeReport rejected timeReport\n * \n * NOTE: If you ever use a property with a leading underscore you are probably making a mistake.\n * But if you must, please ask paul, yarn, bryan or eddie for a CR.\n */\nclass Slice\n{\n /**\n * @param {JobManager} jobManager\n * @param {SliceMessage} sliceMessage\n * @param {object} authorizationMessage\n */\n constructor (jobManager, sliceMessage, authorizationMessage)\n {\n /** @type {Synchronizer} */\n this._state = new Synchronizer(INITIAL, [ INITIAL, READY, RESERVED, WORKING, COMPLETE, FAILED, BROKEN, FINISHED ]);\n /** @type {JobManager} */\n this._jobManager = jobManager;\n /** @type {SliceMessage} */\n this._sliceMessage = { ...sliceMessage };\n this._authorizationMessage = authorizationMessage;\n this._datum = null;\n this._result = null;\n this._error = null;\n /** @type {number} */\n this.startTime = 0;\n /** @type {number} */\n this.rejectedTimeStamp = null;\n /** @type {{ total: number, CPU: number, webGL: number }} */\n this.rejectedTimeReport = null;\n \n assert(this.jobAddress === String(this._sliceMessage.jobAddress));\n if (!this.authorizationMessage && this.sliceNumber > 0)\n throw new Error(`Undefined authorization for slice ${this.identifier}.`);\n \n const that = this;\n \n /** \n * Start loading dependencies in the background. Once these are loaded, this.state will \n * transition to READY and the job will be ready to transition to WORKING.\n */\n (async function supervisor$$slice$$loadDatum() {\n ({ inputDatum: that._datum, dataError: that._error } = await that.jobManager.fetchSliceData(that.datumUri, that));\n }) (/* iife */)\n .then (() => {\n debugging('slice') && console.debug('Slice is transitioning to READY');\n this.state.set(INITIAL, READY);\n })\n .catch((error) => {\n debugging('slice') && console.debug('jobManager.fetchSliceData failed', error);\n this.state.set(INITIAL, BROKEN);\n })\n .finally(() => {\n debugging('slice') && console.debug('Slice.loadDatum completed.', this.identifier);\n });\n }\n\n /** @type {Synchronizer} */\n get state () { return this._state; }\n /** @type {number} */\n get sliceNumber () { return this._sliceMessage.sliceNumber; }\n /** @type {string} */\n get jobAddress () { return this._jobManager.address; }\n /** @type {boolean} */\n get isEstimation () { return this._sliceMessage.isEstimationSlice; }\n /** @type {boolean} */\n get isLong () { return this._sliceMessage.isLongSlice; }\n /** @type {string} */\n get datumUri () { return this._sliceMessage.datumUri; }\n /** @type {JobManager} */\n get jobManager () { return this._jobManager; }\n /** @type {string} */\n get resultStorageType () { return this._sliceMessage.resultStorageType; }\n /** @type {string} */\n get resultStorageDetails () { return this._sliceMessage.resultStorageDetails; }\n\n /** Read-only properties of type object. */\n get datum () { return this._datum; }\n get result () { return this._result; }\n get error () { return this._error; }\n get resultStorageParams () { return this._sliceMessage.resultStorageParams; }\n get authorizationMessage () { return this._authorizationMessage; }\n\n /** @type {boolean} */\n get isQueued () { return this.isUnassigned || this.isReady; }\n /** @type {boolean} */\n get isActive () { return this.isReserved || this.isWorking || this.isComplete; }\n /** @type {boolean} */\n get isFinished () { return this.state.is(FINISHED); }\n /** @type {boolean} */\n get hasFailed () { return this.state.is(FAILED) || this.state.is(BROKEN); }\n /** @type {boolean} */\n get isComplete () { return this.state.is(COMPLETE); }\n /** @type {boolean} */\n get isUnassigned () { return this.state.is(INITIAL); }\n /** @type {boolean} */\n get isReady () { return this.state.is(READY); }\n /** @type {boolean} */\n get isWorking () { return this.state.is(WORKING); }\n /** @type {boolean} */\n get isReturnable () { return this.state.is(WORKING); }\n /**\n * Mark a slice as RESERVED to remove it from the ready list, yet still able to transition to WORKING.\n * @type {boolean}\n **/\n get isReserved () { return this.state.is(RESERVED); }\n\n // TEMPORARY: THIS IS ONLY USED FOR COMPATIBILITY WITH SUPERVISOR1.\n // WHEN SUPERVISOR1 IS PUT TO BED, REMOVE THESE TWO FUNCTIONS.\n /** DO NOT USE! @deprecated @type {boolean} */\n get failed () { return this.hasFailed; }\n /** DO NOT USE! @deprecated @type {boolean} */\n get completed () { return this.isComplete; }\n \n /** @type {string} */\n get identifier () { return `${this.sliceNumber}.${truncateAddress(this.jobAddress, addressTruncationLength)}.${this.state}`; }\n /** \n * timeReport accessor that optionally updates from this.rejectedTimeReport. \n * @type { { total, CPU, webGL } }\n **/\n get timeReport ()\n {\n if (this.result.timeReport && this.rejectedTimeReport && this.rejectedTimeReport.total > 0)\n {\n // Data collected from sandboxes that rejected this slice.\n ['total', 'CPU', 'webGL'].forEach((key) => {\n if (this.rejectedTimeReport[key]) this.result.timeReport[key] += this.rejectedTimeReport[key];\n });\n this.rejectedTimeReport = null;\n }\n return this.result.timeReport;\n }\n /**\n * Return the time interval to estimated slice completion time.\n * @type {number}\n **/\n get etaMs ()\n {\n if (this.startTime === null) return 0;\n let etaMs = this.jobManager.estimateWallMs;\n if (this.startTime) etaMs -= (Date.now() - this.startTime);\n return etaMs;\n }\n\n /** Start slice over, regardless of what state it is in. */\n resetState() { this._state = new Synchronizer(READY, [ INITIAL, READY, RESERVED, WORKING, COMPLETE, FAILED, BROKEN ]); }\n\n /** \n * Sets the slice status to RESERVED, called to remove slice from the ready list,\n * yet still able to transition to WORKING.\n **/\n markAsReserved() { this.state.set(READY, RESERVED); }\n\n /** Sets the slice status to WORKING, called when the slice is getting ready to be handed to a worker. */\n markAsWorking() { this.state.set(RESERVED, WORKING); }\n\n /** Sets the slice status to FINISHED, called when the slice has completed and submitted results. */\n markAsFinished() { this.state.set(COMPLETE, FINISHED); }\n\n /**\n * Verifies slice status is working and assign sandbox, called when the slice is handed to a worker.\n */\n verifyWorking()\n {\n if (!this.isWorking) {\n throw new Error(`Slice ${this.identifier} is not working.`);\n }\n }\n\n /**\n * Receives a result from the scheduler.\n * It will then put the result in the appropriate place.\n * It could also eventually determine if the slice should be\n * retried before determining that it has failed.\n *\n * @param {object|Error} result - The result that came back from the worker sandbox.\n * @param {boolean} [success=true] - True if result is considered successful, false if error occurred.\n */\n collectResult(result, success = true)\n {\n if (this.result)\n throw new Error(`Slice ${this.identifier} received more than one result.`);\n \n if (success)\n {\n this.state.set(WORKING, COMPLETE);\n this._result = result;\n }\n else\n {\n this.state.set(WORKING, FAILED);\n this._error = result;\n }\n debugging('slice') && console.debug('collectResult', this.identifier);\n }\n\n /**\n * Create basic message object as part of the payload to send back to the result-submitter's status operation.\n * @param {string} status - The kind of status operation\n * @param {object} [extraProperties={}] - Extra properties for the paylaod object.\n * @returns {object}\n */\n getMessage(status, extraProperties = {})\n {\n return {\n sliceNumbers: [this.sliceNumber],\n job: this.jobAddress,\n authorizationMessage: this.authorizationMessage,\n status,\n ...extraProperties,\n }; \n }\n\n /**\n * Create basic payload object to send back to the result-submitter's status operation.\n * @param {opaqueId} worker - The current worker's opaqueId\n * @param {string} status - The kind of status operation\n * @param {object} [extraProperties={}] - Extra properties for the paylaod object.\n * @returns {object}\n */\n getMessagePayload(worker, status, extraProperties = {})\n {\n return {\n worker,\n slices: [ this.getMessage(status, extraProperties) ],\n }; \n }\n\n /**\n * Create slice-return payload object to send to the result-submitter's status operation.\n * @param {opaqueId} worker - The current worker's opaqueId\n * @param {string} [reason] - Optional reason for the return: 'ENOPROGRESS', 'EUNCAUGHT', 'ESLICETOOSLOW', 'unknown'.\n * @return {object}\n */\n getReturnMessagePayload(worker, reason)\n {\n delete this._result;\n\n if (!reason) reason = this.error ? 'EUNCAUGHT' : 'unknown';\n const extraProperties = {\n isEstimationSlice: this.isEstimation,\n error: this.error,\n reason,\n };\n\n return this.getMessagePayload(worker, 'return', extraProperties);\n }\n\n /**\n * @deprecated -- DO NOT USE!\n * TEMPORARY: THIS IS ONLY USED FOR COMPATIBILITY WITH SUPERVISOR1.\n * WHEN SUPERVISOR1 IS PUT TO BED, REMOVE THIS FUNCTION.\n * This function helps enforce the equivalence:\n * !this.authorizationMessage <==> sliceNumber === 0 .\n * @returns {object} this.authorizationMessage\n */\n getAuthorizationMessage () {\n if (!this.authorizationMessage && this.sliceNumber > 0)\n throw new Error(`Undefined authorization for slice ${this.identifier}.`);\n return this.authorizationMessage;\n }\n}\nexports.Slice = Slice;\n\n\n//# sourceURL=webpack://dcp/./src/dcp-client/worker/supervisor2/slice2.js?");
4460
4590
 
4461
4591
  /***/ }),
4462
4592
 
@@ -4467,7 +4597,7 @@ eval("/* provided dependency */ var process = __webpack_require__(/*! ./node_mod
4467
4597
  /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
4468
4598
 
4469
4599
  "use strict";
4470
- eval("/* provided dependency */ var process = __webpack_require__(/*! ./node_modules/process/browser.js */ \"./node_modules/process/browser.js\");\n/**\n * @file debugging.js\n * Utility functions for improving debug output, etc, in DCP JavaScript programs.\n *\n * @author Wes Garland, wes@kingsds.network\n * @date Nov 2019\n */\n\n\nconst { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\nconst { setenv, getenv } = __webpack_require__(/*! ./common/dcp-env */ \"./src/common/dcp-env.js\");\n\nvar selfDebugging = false;\nvar scopeList = [];\n\n/**\n * Declare a new debugging scope. Each scope can watch a separate watchObject to determine\n * whether or not we are currently in debug mode. Every scope watches the environment \n * variable 'DCP_DEBUG'. \n *\n * The debugging scope is queried by invoking the returned function, which has an optional\n * 'subsystem' argument. When the query is made without the subsystem argument, the returned\n * function returns true if any subsystem in the scope is being debugged. Otherwise, we return\n * true when the current debugging operations matches the debugging scope and specified subsystem.\n *\n * The watch object and the DCP_DEBUG environment variable are re-evaluated every time the \n * returned function is called, however the results of matching for a given scope/subsystem are \n * cached for speed.\n *\n * @param {string} scopeName The name of the debugging scope (eg. 'compute')\n * @param {object} [watchObject={}] An optional object whose `debug` property is watched\n * to determine what debugging operation we are performing.\n * @returns {function} A function which returns boolean which is only true when the current\n * debugging operation describes both the current scope and subsystem.\n *\n * @example\n * // logs when watchObject.debug or DCP_DEBUG='compute' or '*'\n * debugging = require('debugging').scope('compute');\n * debugging() && console.log('hello');\n *\n * @example \n * // logs when watchObject.debug or DCP_DEBUG='compute:for' or '*'\n * debugging = require('debugging').scope('compute');\n * debugging('for') && console.log('hello');\n * \n * @example\n * // Note that environment variables can be set in local/session storage for a \n * // browser. The following is equivalent to DCP_DEBUG='dcp:*'\n * localStorage.setItem(\"env\", JSON.stringify({\"DCP_DEBUG\": \"dcp:*\"}));\n * // in source code, both of these will log.\n * debugging = require('debugging').scope('dcp');\n * debugging('sender') && console.debug('hello');\n * debugging('receiver') && console.debug('world');\n */\nexports.scope = function debugging$$scope(scopeName, watchObject = {})\n{\n var debugSyms;\n var disabled=false;\n\n if (!scopeName)\n throw new Error('Must specify scopeName!');\n\n selfDebugging && console.log(`debugging ${scopeName} - create scope`);\n\n function initializeScope()\n {\n const env = getenv('DCP_DEBUG');\n\n debugSyms = (env || '') + (env && watchObject.debug ? ',' : '') + (watchObject.debug || '');\n debugging.cache = {};\n debugging.cacheValidity = debugSyms;\n\n selfDebugging && console.log(`debugging ${scopeName} - initialized lookup cache`);\n }\n\n function debugging(subsystem) {\n var re;\n\n if (disabled === true)\n return false;\n\n if (!selfDebugging && debugging.cache.hasOwnProperty(subsystem))\n return debugging.cache[subsystem];\n\n /* No self-debugging code above to keep cache-hit and watch-object paths fast */\n \n let scopeMatch, subsysMatch;\n loop: for (let debugSym of debugSyms.split(',')) {\n scopeMatch = (debugSym === scopeName || debugSym.startsWith(scopeName + ':'));\n subsysMatch = false || (!subsystem);\n\n selfDebugging && console.log(`debugging ${scopeName}:${subsysMatch ? '(weak)' : subsystem} - examining debugSym ${debugSym}`);\n\n if (scopeMatch && subsysMatch) {\n selfDebugging && console.log(`debugging ${scopeName} - early match`);\n break loop;\n }\n\n if (typeof debugSym === 'boolean' && debugSym) {\n scopeMatch = debugSym;\n selfDebugging && console.log(`debugging ${scopeName} - matched on boolean debugSym`);\n break;\n }\n\n switch(debugSym)\n {\n case '*': \n case 'all':\n selfDebugging && console.log(`debugging ${scopeName} - ${debugSym} matched scope`);\n scopeMatch = scopeName !== 'debugging';\n break loop;\n case '*:*':\n case 'all:*':\n case '*:all':\n case 'all:all':\n selfDebugging && console.log(`debugging ${scopeName} - ${debugSym} matched everything`);\n scopeMatch = subsysMatch = scopeName !== 'debugging';\n break loop;\n case scopeName + ':all' : /* everything for this scope */\n selfDebugging && console.log(`debugging ${scopeName} - ${debugSym} matched everything for this scope`);\n scopeMatch = subsysMatch = true;\n break loop;\n case scopeName + ':*' : \n case 'all:' + subsystem:\n case '*:' + subsystem: /* everything for this subsystem */\n selfDebugging && console.log(`debugging - ${debugSym} matched everything for this subsystem`);\n scopeMatch = scopeName !== 'debugging';\n subsysMatch = true;\n break loop;\n case scopeName + ':' + subsystem: /* things for this scope and subsystem */\n selfDebugging && console.log(`debugging - ${debugSym} matched scope and subsystem`);\n scopeMatch = subsysMatch = true;\n break loop;\n }\n\n re = new RegExp('^(' + debugSym + ')$');\n selfDebugging && console.log(`debugging ${scopeName} - checking regular expression ${re}`);\n if (re.test(scopeName)) {\n selfDebugging && console.log(`debugging ${scopeName} - ${debugSym} matched regular expression for scope`);\n scopeMatch = true;\n break loop;\n }\n else if (re.test(subsystem)) {\n selfDebugging && console.log(`debugging ${scopeName} - ${debugSym} matched regular expression for subsystem`);\n subsysMatch = true;\n break loop;\n }\n else if (re.test(scopeName + ':' + subsystem)) {\n selfDebugging && console.log(`debugging ${scopeName} - ${debugSym} matched regular expression for subsystem`);\n scopeMatch = subsysMatch = true;\n break loop;\n }\n\n if (scopeMatch && subsysMatch)\n break loop;\n\n selfDebugging && console.log(`debugging ${scopeName} - ${debugSym} => no match for ${scopeName}:${subsystem}`);\n } /* loop */\n \n return debugging.cache[subsystem] = (scopeMatch && subsysMatch);\n }\n\n if (scopeList.length === 0)\n {\n process.on('SIGHUP', () => exports.reinit());\n process.on('SIGUSR2', () => exports.toggle());\n }\n \n scopeList.push({\n toggle: () => disabled = !disabled,\n set: (enabled) => disabled = !enabled,\n reinit: () => initializeScope()\n });\n\n initializeScope();\n\n return debugging;\n}\n\n/**\n * Re-read and maybe re-specify DCP_DEBUG. This causes all running debugging()\n * scopes in the current program to reinitialize, allowing dynamic changes.\n *\n * @param {string} DCP_DEBUG if defined, changes the value of DCP_DEBUG environment variable\n */\nexports.reinit = function reinit(DCP_DEBUG)\n{\n if (typeof DCP_DEBUG !== 'undefined')\n setenv('DCP_DEBUG', DCP_DEBUG);\n\n console.debug(`Reinitialize debugging scopes; DCP_DEBUG=${getenv('DCP_DEBUG')}`);\n scopeList.forEach(scope => scope.reinit())\n}\n\n/** Toggle all scopes on/off */\nexports.toggle = function toggle() {\n scopeList.forEach(scope => scope.toggle());\n}\n\n/** Set all scopes on/off */\nexports.set = function set(enabled) {\n scopeList.forEach(scope => scope.set(enabled));\n if (enabled)\n console.debug('set debugging enabled');\n else\n console.debug('set debugging disabled');\n}\n\nselfDebugging = exports.scope('debugging')();\n\n\n//# sourceURL=webpack://dcp/./src/debugging.js?");
4600
+ eval("/* provided dependency */ var process = __webpack_require__(/*! ./node_modules/process/browser.js */ \"./node_modules/process/browser.js\");\n/**\n * @file debugging.js\n * Utility functions for improving debug output, etc, in DCP JavaScript programs.\n *\n * @author Wes Garland, wes@kingsds.network\n * @date Nov 2019\n */\n\n\nconst { setenv, getenv } = __webpack_require__(/*! ./common/dcp-env */ \"./src/common/dcp-env.js\");\n\nvar selfDebugging = false;\nvar scopeList = [];\n\n/**\n * Declare a new debugging scope. Each scope can watch a separate watchObject to determine\n * whether or not we are currently in debug mode. Every scope watches the environment \n * variable 'DCP_DEBUG'. \n *\n * The debugging scope is queried by invoking the returned function, which has an optional\n * 'subsystem' argument. When the query is made without the subsystem argument, the returned\n * function returns true if any subsystem in the scope is being debugged. Otherwise, we return\n * true when the current debugging operations matches the debugging scope and specified subsystem.\n *\n * The watch object and the DCP_DEBUG environment variable are re-evaluated every time the \n * returned function is called, however the results of matching for a given scope/subsystem are \n * cached for speed.\n *\n * @param {string} scopeName The name of the debugging scope (eg. 'compute')\n * @param {object} [watchObject={}] An optional object whose `debug` property is watched\n * to determine what debugging operation we are performing.\n * @returns {function} A function which returns boolean which is only true when the current\n * debugging operation describes both the current scope and subsystem.\n *\n * @example\n * // logs when watchObject.debug or DCP_DEBUG='compute' or '*'\n * debugging = require('debugging').scope('compute');\n * debugging() && console.log('hello');\n *\n * @example \n * // logs when watchObject.debug or DCP_DEBUG='compute:for' or '*'\n * debugging = require('debugging').scope('compute');\n * debugging('for') && console.log('hello');\n * \n * @example\n * // Note that environment variables can be set in local/session storage for a \n * // browser. The following is equivalent to DCP_DEBUG='dcp:*'\n * localStorage.setItem(\"env\", JSON.stringify({\"DCP_DEBUG\": \"dcp:*\"}));\n * // in source code, both of these will log.\n * debugging = require('debugging').scope('dcp');\n * debugging('sender') && console.debug('hello');\n * debugging('receiver') && console.debug('world');\n */\nexports.scope = function debugging$$scope(scopeName, watchObject = {})\n{\n var debugSyms;\n var disabled=false;\n\n if (!scopeName)\n throw new Error('Must specify scopeName!');\n\n selfDebugging && console.log(`debugging ${scopeName} - create scope`);\n\n function initializeScope()\n {\n const env = getenv('DCP_DEBUG');\n\n debugSyms = (env || '') + (env && watchObject.debug ? ',' : '') + (watchObject.debug || '');\n debugging.cache = {};\n debugging.cacheValidity = debugSyms;\n\n selfDebugging && console.log(`debugging ${scopeName} - initialized lookup cache`);\n }\n\n function debugging(subsystem) {\n var re;\n\n if (disabled === true)\n return false;\n\n if (!selfDebugging && debugging.cache.hasOwnProperty(subsystem))\n return debugging.cache[subsystem];\n\n /* No self-debugging code above to keep cache-hit and watch-object paths fast */\n \n let scopeMatch, subsysMatch;\n loop: for (let debugSym of debugSyms.split(',')) {\n scopeMatch = (debugSym === scopeName || debugSym.startsWith(scopeName + ':'));\n subsysMatch = false || (!subsystem);\n\n selfDebugging && console.log(`debugging ${scopeName}:${subsysMatch ? '(weak)' : subsystem} - examining debugSym ${debugSym}`);\n\n if (scopeMatch && subsysMatch) {\n selfDebugging && console.log(`debugging ${scopeName} - early match`);\n break loop;\n }\n\n if (typeof debugSym === 'boolean' && debugSym) {\n scopeMatch = debugSym;\n selfDebugging && console.log(`debugging ${scopeName} - matched on boolean debugSym`);\n break;\n }\n\n switch(debugSym)\n {\n case '*': \n case 'all':\n selfDebugging && console.log(`debugging ${scopeName} - ${debugSym} matched scope`);\n scopeMatch = scopeName !== 'debugging';\n break loop;\n case '*:*':\n case 'all:*':\n case '*:all':\n case 'all:all':\n selfDebugging && console.log(`debugging ${scopeName} - ${debugSym} matched everything`);\n scopeMatch = subsysMatch = scopeName !== 'debugging';\n break loop;\n case scopeName + ':all' : /* everything for this scope */\n selfDebugging && console.log(`debugging ${scopeName} - ${debugSym} matched everything for this scope`);\n scopeMatch = subsysMatch = true;\n break loop;\n case scopeName + ':*' : \n case 'all:' + subsystem:\n case '*:' + subsystem: /* everything for this subsystem */\n selfDebugging && console.log(`debugging - ${debugSym} matched everything for this subsystem`);\n scopeMatch = scopeName !== 'debugging';\n subsysMatch = true;\n break loop;\n case scopeName + ':' + subsystem: /* things for this scope and subsystem */\n selfDebugging && console.log(`debugging - ${debugSym} matched scope and subsystem`);\n scopeMatch = subsysMatch = true;\n break loop;\n }\n\n re = new RegExp('^(' + debugSym + ')$');\n selfDebugging && console.log(`debugging ${scopeName} - checking regular expression ${re}`);\n if (re.test(scopeName)) {\n selfDebugging && console.log(`debugging ${scopeName} - ${debugSym} matched regular expression for scope`);\n scopeMatch = true;\n break loop;\n }\n else if (re.test(subsystem)) {\n selfDebugging && console.log(`debugging ${scopeName} - ${debugSym} matched regular expression for subsystem`);\n subsysMatch = true;\n break loop;\n }\n else if (re.test(scopeName + ':' + subsystem)) {\n selfDebugging && console.log(`debugging ${scopeName} - ${debugSym} matched regular expression for subsystem`);\n scopeMatch = subsysMatch = true;\n break loop;\n }\n\n if (scopeMatch && subsysMatch)\n break loop;\n\n selfDebugging && console.log(`debugging ${scopeName} - ${debugSym} => no match for ${scopeName}:${subsystem}`);\n } /* loop */\n\n return (debugging.cache[subsystem] = (scopeMatch && subsysMatch));\n }\n\n if (scopeList.length === 0)\n {\n process.on('SIGHUP', () => exports.reinit());\n process.on('SIGUSR2', () => exports.toggle());\n }\n \n scopeList.push({\n toggle: () => (disabled = !disabled),\n set: (enabled) => (disabled = !enabled),\n reinit: () => initializeScope()\n });\n\n initializeScope();\n\n return debugging;\n}\n\n/**\n * Re-read and maybe re-specify DCP_DEBUG. This causes all running debugging()\n * scopes in the current program to reinitialize, allowing dynamic changes.\n *\n * @param {string} DCP_DEBUG if defined, changes the value of DCP_DEBUG environment variable\n */\nexports.reinit = function reinit(DCP_DEBUG)\n{\n if (typeof DCP_DEBUG !== 'undefined')\n setenv('DCP_DEBUG', DCP_DEBUG);\n\n selfDebugging && console.debug(`Reinitialize debugging scopes; DCP_DEBUG=${getenv('DCP_DEBUG')}`);\n scopeList.forEach(scope => scope.reinit())\n}\n\n/** Toggle all scopes on/off */\nexports.toggle = function toggle() {\n scopeList.forEach(scope => scope.toggle());\n}\n\n/** Set all scopes on/off */\nexports.set = function set(enabled) {\n scopeList.forEach(scope => scope.set(enabled));\n if (enabled)\n selfDebugging && console.debug('set debugging enabled');\n else\n selfDebugging && console.debug('set debugging disabled');\n}\n\nselfDebugging = exports.scope('debugging')();\n\n\n//# sourceURL=webpack://dcp/./src/debugging.js?");
4471
4601
 
4472
4602
  /***/ }),
4473
4603
 
@@ -4488,7 +4618,7 @@ eval("/**\n * @file events/event-subscriber.js\n * @author Ryan Rossiter
4488
4618
  /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
4489
4619
 
4490
4620
  "use strict";
4491
- eval("/* provided dependency */ var Buffer = __webpack_require__(/*! ./node_modules/node-polyfill-webpack-plugin/node_modules/buffer/index.js */ \"./node_modules/node-polyfill-webpack-plugin/node_modules/buffer/index.js\")[\"Buffer\"];\n/**\n * @file pseudo-express.js\n *\n * A lightweight url->callback routing scheme which uses a syntax very similar to\n * a subset of ExpressJS v4. This was designed explicitly for interoperation with\n * the Target connection class and SocketIO, but could - in theory - be used with \n * any NodeJS http server. \n *\n * @author Wes Garland, wes@kingsds.network\n * @date July 2021\n */\n\n\nconst debugging = (__webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope)('pseudo-express');\n\n/**\n * @constructor\n * Create, in ExpressJS terminology, an \"app\" or \"router\" that is bound to the given server.\n *\n * @param {object} httpServer [optional] server to bind to \n */\nexports.PseudoExpress = function PseudoExpress(httpServer)\n{\n this._routes = [];\n\n if (httpServer)\n this.bindServer(httpServer);\n}\n\n/**\n * API to bind an instance to a specific http server. More than one server per instance is supported, just call\n * this function multiple times.\n *\n * @param {object} server a node http or https Server\n * @param {string} [handledMarker] replaces request.url if the request has been routed successfully.\n */\nexports.PseudoExpress.prototype.bindServer = function PseudoExpress$bindServer(server, handledMarker)\n{\n if (!handledMarker)\n handledMarker = 'pseudo-express-handled';\n\n server.prependListener('request', (request, response) => this.routeRequest(request, response, handledMarker));\n server.on('request', (request, response) => {\n debugging('requests') && console.debug(`pe: finished processing ${request.url}`);\n if (request.url === handledMarker || request.handleOutsideExpress)\n return;\n \n setImmediate(() => {\n if (response.headersSent)\n console.warn('Warning: rogue listener handled request', request.url);\n else\n {\n debugging() && console.debug(`pe: request ${request.url} => 404`);\n sendHttpError(request, response, 404, `${request.url} not found.`);\n }\n });\n });\n}\n\n/**\n * Internal function to mark a request as routed.\n * @param {object} request Node HTTP request object\n * @param {string} handledMarker see PseudoExpress$bindServer\n */\nexports.PseudoExpress.prototype.markRequest = function PseudoExpress$markRequest(request, handledMarker)\n{\n debugging('requests') && console.debug('pe: marking request', request.url, 'handled via', handledMarker);\n request.url = handledMarker; \n}\n\n/**\n * Route a request to a request handler. We match request.url against previously-registered routes\n * (eg with .get) and run the first handler we find, then we mark the request (mutate request.url)\n * so that no other request handlers (I'm looking at you, socket.io) will want to touch it.\n *\n * If a request handler throws an exception, we handle the request by responding with an HTTP error.\n *\n * @param {object} request Node HTTP request object\n * @param {object} response Node HTTP response object\n * @param {string} handledMarker see PseudoExpress$bindServer\n */\nexports.PseudoExpress.prototype.routeRequest = function PseudoExpress$routeRequest(request, response, handledMarker)\n{\n var retval;\n \n try\n {\n debugging() && console.debug(`pe: routing ${request.url}`);\n for (let i=0; i < this._routes.length; i++) \n {\n let pathMatchRe = this._routes[i].match;\n \n if (!pathMatchRe.test(request.url))\n debugging('requests') && !debugging('all') && console.debug('pe: does not match', pathMatchRe)\n else\n {\n if (this._routes[i].handleOutsideExpress)\n {\n request.handleOutsideExpress = true\n break\n }\n else\n {\n pseudoExpressRoute(this._routes[i], request, response);\n this.markRequest(request, handledMarker);\n }\n break;\n }\n }\n }\n catch(error)\n {\n sendHttpError(request, response, 500, `Error processing ${request.url}\\n` + error.stack);\n this.markRequest(handledMarker);\n }\n}\n\n/**\n * @param {RegExp} match regular expression that matches request url (pathname + search)\n */\n/**\n * Register an interceptor for the HTTP get method\n *\n * @param {string} match string that matches request pathname exactly\n * @param {function} callback function to run when the route matches\n * @param {Boolean} handleOutsideExpress [optional] set to true if this route should be handled externally from the pseudoExpressRoute\n */\nexports.PseudoExpress.prototype.get = function PseudoExpress$get(match, callback, handleOutsideExpress = false)\n{\n if (typeof match === 'string')\n match = new RegExp(`^${match}($|\\\\?)`);\n\n debugging('requests') && console.debug(`pe: added GET method intercept ${match}: ${callback.name}`);\n this._routes.push({\n method: 'get',\n match,\n callback,\n handleOutsideExpress,\n });\n}\n\n/**\n * Register an interceptor for the HTTP post method\n * @see PseudoExpress$get\n * @param {Boolean} handleOutsideExpress [optional] set to true if this route should be handled externally from the pseudoExpressRoute\n */\nexports.PseudoExpress.prototype.post = function PseudoExpress$post(match, callback, handleOutsideExpress = false)\n{\n if (typeof match === 'string')\n match = new RegExp(`^${match}$`);\n\n debugging('requests') && console.debug(`pe: added POST method intercept ${match}: ${callback.name}`);\n this._routes.push({\n method: 'post',\n match,\n callback,\n handleOutsideExpress,\n });\n}\n\n/**\n * Register an interceptor for all HTTP methods\n * @see PseudoExpress$get\n * @param {Boolean} handleOutsideExpress [optional] set to true if this route should be handled externally from the pseudoExpressRoute\n */\nexports.PseudoExpress.prototype.all = function PseudoExpress$all(match, callback, handleOutsideExpress = false)\n{\n if (typeof match === 'string')\n match = new RegExp(`^${match}$`);\n\n debugging('requests') && console.debug(`pe: added all-method intercept ${match}: ${callback.name}`);\n this._routes.push({\n method: 'all',\n match,\n callback,\n handleOutsideExpress,\n });\n}\n\n/**\n * Register a middleware callback\n * @see PseudoExpress$get\n * @param {function} next third argument callback function\n */\nexports.PseudoExpress.prototype.use = function PseudoExpress$use(match, callback, next)\n{\n if (typeof match === 'string')\n match = new RegExp(`^${match}$`);\n\n function peUseCallbackWrapper(request, result)\n {\n callback(request, result, next);\n return; /* don't cancel bubble so we can chain to other routes */\n }\n\n debugging('requests') && console.debug(`pe: added use intercept ${match}: ${callback.name}`);\n this._routes.push({\n method: 'all',\n match,\n callback: peUseCallbackWrapper,\n retval: true\n });\n}\n\nfunction sendHttpError(request, response, code, text)\n{\n const corsHeaders = {\n 'access-control-allow-origin': '*',\n 'access-control-allow-headers': 'content-type',\n };\n\n response.setHeader('content-type', 'text/plain');\n response.setHeader('content-type', 'text/plain; charset=utf8');\n response.setHeader('cache-control', 'no-cache');\n for (let header in corsHeaders)\n response.setHeader(header, corsHeaders[header]);\n \n response.writeHead(code);\n response.write(`${code} - ${text}`);\n}\n\n/**\n * Utility function for use by the PseudoExpress class. Actually implements the \"routing\" behaviour.\n *\n * @param {object} descriptor mapping for the behaviour of the \"express\" rule - e.g., \n * send all GET queries at location /status to the status handler\n * @param {object} httpRequest the http request to handle, from the node http or https modules\n * @param {object} httpResponse the response object associated with httpRequest\n */\nfunction pseudoExpressRoute(descriptor, httpRequest, httpResponse)\n{\n const rMethod = httpRequest.method.toLowerCase();\n const request = new PseudoExpressRequest (httpRequest);\n const response = new PseudoExpressResponse(httpResponse, request);\n \n if (descriptor.method !== 'all' && descriptor.method !== rMethod)\n return; /* try next handler */\n\n try\n {\n if (rMethod === 'get')\n {\n let i = httpRequest.url.indexOf('?');\n request.query = {};\n \n if (i !== -1) /* query string present - decode */\n {\n let queryString = httpRequest.url.slice(i + 1);\n for (let kvp of queryString.split('&'))\n {\n let [ key, value ] = kvp.split('=');\n request.query[key] = value && decodeURIComponent(value.replace(/\\+/g, ' '));\n }\n }\n descriptor.callback(request, response);\n }\n else if (rMethod === 'post')\n {\n const chunks = [];\n request.on('data', chunk => chunks.push(chunk));\n request.on('end', function pseudoExpress$postEnd() {\n request.body = Buffer.concat(chunks);\n descriptor.callback(request, response);\n });\n }\n else\n {\n descriptor.callback(request, response);\n }\n }\n catch(error)\n {\n dumpError(request, response, error);\n }\n \n if (descriptor.hasOwnProperty('retval'))\n return descriptor.retval;\n}\n\n/**\n * @constructor\n * Class to represent incoming http request\n * @param {object} httpRequest NodeJS request from httpServer\n */\nfunction PseudoExpressRequest(httpRequest)\n{\n Object.assign(this, httpRequest);\n this.__httpRequest = httpRequest;\n this.originalUrl = this.__httpRequest.url;\n this.path = this.__httpRequest.url.replace(/\\?.*$/, '');\n this.hostname = this.__httpRequest.headers.host;\n}\n\n/**\n * @constructor\n * Class to represent outgoing http response\n */\nfunction PseudoExpressResponse(httpResponse, request)\n{\n this.httpResponse = httpResponse;\n this.headers = {};\n this.request = request;\n this.headersSent = false;\n}\n\n/**\n * @param {object} field Object containing multiple fields and values\n */\n/**\n * Sets the response’s HTTP header field to value. To set multiple fields at once, pass an \n * object as the parameter.\n * \n * @note Headers are collapsed to lower case\n *\n * @param {string} field Header name\n * @param {string} value Value of header\n */\nPseudoExpressResponse.prototype.set = function PseudoExpressResponse$$set(field, value)\n{\n if (typeof field === 'string') /* single header */\n this.headers[field.toLowerCase()] = value;\n else /* assume Object => multiple headers */\n {\n for (let entry of Object.values(field))\n this.headers[entry.field.toLowerCase()] = entry.value;\n }\n\n return this;\n}\n\n/** \n * Returns the HTTP response header specified by field. The match is case-insensitive.\n *\n * @param {string} field\n * @returns {string}\n */\nPseudoExpressResponse.prototype.get = function PseudoExpressResponse$$get(field)\n{\n return this.headers[field.toLowerCase()];\n}\n\n/**\n * Send HTTP status and headers immediately, without the respsonse body.\n *\n * @note Express does not appear to have an API that does this. It is needed to allow\n * streaming content.\n *\n * @param {object} defaultHeaders Headers to send as though they were in an object passed to\n * the set() method, unless they were already defined elsewhere\n * @param {object|string} body The body, per .send(), for the message; only used to detect\n * default header params; is not actually used in the response.\n * @returns this, to allow chaining\n */\nPseudoExpressResponse.prototype.sendHeaders = function PseudoExpressResponse$$sendHeaders(defaultHeaders, body)\n{\n var defaultContentType;\n\n if (!body)\n body = this.body;\n\n if (!this.headers['content-type'])\n {\n switch(typeof body)\n {\n case 'object':\n {\n if (Buffer.isBuffer(body))\n defaultContentType = 'application/octet-stream';\n else\n defaultContentType = 'application/json';\n break;\n }\n case 'string': \n defaultContentType = 'text/plain; charset=utf-8';\n break;\n default:\n defaultContentType = 'application/octet-stream';\n break;\n }\n }\n \n this.httpResponse.writeHead(this.statusCode || 200, Object.assign({ 'content-type': defaultContentType }, defaultHeaders, this.headers))\n this.headersSent = true;\n\n return this;\n}\n\n/**\n * Sends the HTTP response.\n * The body parameter can be a Buffer object, a String, an object. Plain objects are serialized via\n * JSON.stringify. This method performs many useful tasks for simple non-streaming responses: For \n * example, it automatically assigns the Content-Length HTTP response header field (unless previously \n * defined).\n * \n * When the parameter is a Buffer object, the method sets the Content-Type response header field \n * to “application/octet-stream”, unless previously defined.\n *\n * @param {string} body The body to send as the response.\n * @returns this, to allow chaining\n */\nPseudoExpressResponse.prototype.send = function PseudoExpressResponse$$send(body)\n{\n var defaultHeaders = {};\n var contentLength;\n var sendBody = body;\n var argv = Array.from(arguments);\n \n if (typeof sendBody === 'object' && sendBody !== null)\n {\n if (Buffer.isBuffer(sendBody))\n contentLength = Buffer.byteLength(sendBody);\n else\n sendBody = JSON.stringify(sendBody);\n }\n argv[0] = sendBody || '';\n\n if (typeof contentLength === 'undefined' && sendBody && typeof sendBody.length !== 'undefined')\n contentLength = sendBody.length;\n\n if (typeof contentLength !== 'undefined')\n defaultHeaders['content-length'] = contentLength;\n \n if (!this.headersSent)\n this.sendHeaders(defaultHeaders, body);\n\n try {\n this.httpResponse.end.apply(this.httpResponse, argv);\n }\n catch (error) {\n if (error.code === 'ERR_STREAM_WRITE_AFTER_END')\n console.warn('Pseudo-express tried to write after end. Suppressing here so we don\\'t crash');\n else {\n console.error('Unexpected error in pseudo-express', error);\n throw error;\n }\n\n }\n\n return this;\n}\n\n/**\n * Ends the response process. This method actually comes from Node core, specifically the \n * response.end() method of http.ServerResponse.\n *\n * Use to quickly end the response without any data. If you need to respond with data, instead \n * use methods such as res.send() and res.json().\n *\n * @param {string} [optional] _data\n * @param {string} [optional] _encoding\n *\n * @returns this, to allow chaining\n */\nPseudoExpressResponse.prototype.end = function PseudoExpressResponse$$end(_data, _encoding)\n{\n this.httpResponse.end.apply(this.httpResponse, arguments);\n\n return this;\n}\n\n/** \n * Sends a JSON response. This method sends a response (with the correct content-type) that is the \n * parameter converted to a JSON string using JSON.stringify().\n *\n * @param {object} bodyObject The object to send as the body of the response\n * @returns this, to allow chaining\n */\nPseudoExpressResponse.prototype.json = function PseudeoExpressResponse$$json(bodyObject)\n{\n this.send(JSON.stringify(bodyObject || typeof bodyObject));\n return this;\n}\n\n/**\n * Sets the HTTP status for the response. It is a chainable alias of Node’s response.statusCode.\n *\n * @param {number} statusCode\n * @returns this, to allow chaining\n */\nPseudoExpressResponse.prototype.status = function PseudoExpressResponse$$status(statusCode)\n{\n this.statusCode = statusCode;\n return this;\n}\n \n/**\n * Sets the response Location HTTP header to the specified path parameter.\n * A path value of “back” has a special meaning, it refers to the URL specified in the Referer\n * header of the request. If the Referer header was not specified, it refers to “/”.\n *\n * @param {string|URL|DcpURL} location\n * @returns this, to allow chaining\n */\nPseudoExpressResponse.prototype.location = function PseudoExpressResponse$$location(location)\n{\n if (location === '..')\n location = this.request.url.replace(/\\/[^/]*$/, '') || '/';\n if (location === 'back')\n location = this.request.headers['http-referer'] || '/';\n\n return this.set('location', location).send(`Redirecting to ${location}`);\n}\n\n/**\n * Sets the Content-Type HTTP header to the MIME type as determined by the specified type.\n *\n * If the type starts with 'text/' and the value does not contain a charset attribute, the attribute\n * charset=utf-8 will be added to the value.\n *\n * @param {string} contentType\n * @returns this, to allow chaining\n */\nPseudoExpressResponse.prototype.type = function PseudoExpressResponse$$type(contentType)\n{\n if (contentType.startsWith('text/') && contentType.includes('charset='))\n contentType += '; charset=\"utf-8\"';\n return this.set('content-type', contentType);\n}\n\n/**\n * Redirects to the URL derived from the specified path, with specified status, a positive integer\n * that corresponds to an HTTP status code. If not specified, status defaults to 302.\n *\n * @param {number} [optional] status\n * @param {string|URL|DcpURL} path\n * @returns this, to allow chaining\n */\nPseudoExpressResponse.prototype.redirect = function PseudoExpressResponse$$redirect(status, path)\n{\n if (typeof status !== 'number')\n {\n path = status;\n status = 302;\n }\n\n return this.status(status).location(path);\n}\n\n/* Try to tell user why pseudo-express \"route\" failed */\nfunction dumpError(request, response, error)\n{\n response.set('content-type', 'text/plain; charset=utf8');\n response.set('cache-control', 'no-cache');\n response.set('access-control-allow-origin', '*');\n response.set('access-control-allow-headers', 'content-type');\n response.statusCode = 500;\n response.write(`500 Internal Server Error accessing ${request.url} (${new Date()})\\n\\n` + error.stack);\n}\n\n\n//# sourceURL=webpack://dcp/./src/node-libs/pseudo-express.js?");
4621
+ eval("/* provided dependency */ var Buffer = __webpack_require__(/*! ./node_modules/node-polyfill-webpack-plugin/node_modules/buffer/index.js */ \"./node_modules/node-polyfill-webpack-plugin/node_modules/buffer/index.js\")[\"Buffer\"];\n/**\n * @file pseudo-express.js\n *\n * A lightweight url->callback routing scheme which uses a syntax very similar to\n * a subset of ExpressJS v4. This was designed explicitly for interoperation with\n * the Target connection class and SocketIO, but could - in theory - be used with \n * any NodeJS http server. \n *\n * @author Wes Garland, wes@kingsds.network\n * @date July 2021\n */\n\n\nconst debugging = (__webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope)('pseudo-express');\n\n/**\n * @constructor\n * Create, in ExpressJS terminology, an \"app\" or \"router\" that is bound to the given server.\n *\n * @param {object} httpServer [optional] server to bind to \n */\nexports.PseudoExpress = function PseudoExpress(httpServer)\n{\n this._routes = [];\n\n if (httpServer)\n this.bindServer(httpServer);\n}\n\n/**\n * API to bind an instance to a specific http server. More than one server per instance is supported, just call\n * this function multiple times.\n *\n * @param {object} server a node http or https Server\n * @param {string} [handledMarker] replaces request.url if the request has been routed successfully.\n */\nexports.PseudoExpress.prototype.bindServer = function PseudoExpress$bindServer(server, handledMarker)\n{\n if (!handledMarker)\n handledMarker = 'pseudo-express-handled';\n\n server.prependListener('request', (request, response) => this.routeRequest(request, response, handledMarker));\n server.on('request', (request, response) => {\n debugging('requests') && console.debug(`pe: finished processing ${request.url}`);\n if (request.url === handledMarker || request.handleOutsideExpress)\n return;\n \n setImmediate(() => {\n if (response.headersSent)\n console.warn('Warning: rogue listener handled request', request.url);\n else\n {\n debugging() && console.debug(`pe: request ${request.url} => 404`);\n sendHttpError(request, response, 404, `${request.url} not found.`);\n }\n });\n });\n}\n\n/**\n * Internal function to mark a request as routed.\n * @param {object} request Node HTTP request object\n * @param {string} handledMarker see PseudoExpress$bindServer\n */\nexports.PseudoExpress.prototype.markRequest = function PseudoExpress$markRequest(request, handledMarker)\n{\n debugging('requests') && console.debug('pe: marking request', request.url, 'handled via', handledMarker);\n request.url = handledMarker; \n}\n\n/**\n * Route a request to a request handler. We match request.url against previously-registered routes\n * (eg with .get) and run the first handler we find, then we mark the request (mutate request.url)\n * so that no other request handlers (I'm looking at you, socket.io) will want to touch it.\n *\n * If a request handler throws an exception, we handle the request by responding with an HTTP error.\n *\n * @param {object} request Node HTTP request object\n * @param {object} response Node HTTP response object\n * @param {string} handledMarker see PseudoExpress$bindServer\n */\nexports.PseudoExpress.prototype.routeRequest = function PseudoExpress$routeRequest(request, response, handledMarker)\n{\n var retval;\n \n try\n {\n debugging() && console.debug(`pe: routing ${request.url}`);\n for (let i=0; i < this._routes.length; i++) \n {\n let pathMatchRe = this._routes[i].match;\n \n if (!pathMatchRe.test(request.url))\n debugging('requests') && !debugging('all') && console.debug('pe: does not match', pathMatchRe)\n else\n {\n if (this._routes[i].handleOutsideExpress)\n {\n request.handleOutsideExpress = true\n break\n }\n else\n {\n pseudoExpressRoute(this._routes[i], request, response);\n this.markRequest(request, handledMarker);\n }\n break;\n }\n }\n }\n catch(error)\n {\n sendHttpError(request, response, 500, `Error processing ${request.url}\\n` + error.stack);\n this.markRequest(handledMarker);\n }\n}\n\n/**\n * @param {RegExp} match regular expression that matches request url (pathname + search)\n */\n/**\n * Register an interceptor for the HTTP get method\n *\n * @param {string} match string that matches request pathname exactly\n * @param {function} callback function to run when the route matches\n * @param {Boolean} handleOutsideExpress [optional] set to true if this route should be handled externally from the pseudoExpressRoute\n */\nexports.PseudoExpress.prototype.get = function PseudoExpress$get(match, callback, handleOutsideExpress = false)\n{\n if (typeof match === 'string')\n match = new RegExp(`^${match}($|\\\\?)`);\n\n debugging('requests') && console.debug(`pe: added GET method intercept ${match}: ${callback.name}`);\n this._routes.push({\n method: 'get',\n match,\n callback,\n handleOutsideExpress,\n });\n}\n\n/**\n * Register an interceptor for the HTTP post method\n * @see PseudoExpress$get\n * @param {Boolean} handleOutsideExpress [optional] set to true if this route should be handled externally from the pseudoExpressRoute\n */\nexports.PseudoExpress.prototype.post = function PseudoExpress$post(match, callback, handleOutsideExpress = false)\n{\n if (typeof match === 'string')\n match = new RegExp(`^${match}$`);\n\n debugging('requests') && console.debug(`pe: added POST method intercept ${match}: ${callback.name}`);\n this._routes.push({\n method: 'post',\n match,\n callback,\n handleOutsideExpress,\n });\n}\n\n/**\n * Register an interceptor for all HTTP methods\n * @see PseudoExpress$get\n * @param {Boolean} handleOutsideExpress [optional] set to true if this route should be handled externally from the pseudoExpressRoute\n */\nexports.PseudoExpress.prototype.all = function PseudoExpress$all(match, callback, handleOutsideExpress = false)\n{\n if (typeof match === 'string')\n match = new RegExp(`^${match}$`);\n\n debugging('requests') && console.debug(`pe: added all-method intercept ${match}: ${callback.name}`);\n this._routes.push({\n method: 'all',\n match,\n callback,\n handleOutsideExpress,\n });\n}\n\n/**\n * Register a middleware callback\n * @see PseudoExpress$get\n * @param {function} next third argument callback function\n */\nexports.PseudoExpress.prototype.use = function PseudoExpress$use(match, callback, next)\n{\n if (typeof match === 'string')\n match = new RegExp(`^${match}$`);\n\n function peUseCallbackWrapper(request, result)\n {\n callback(request, result, next);\n return; /* don't cancel bubble so we can chain to other routes */\n }\n\n debugging('requests') && console.debug(`pe: added use intercept ${match}: ${callback.name}`);\n this._routes.push({\n method: 'all',\n match,\n callback: peUseCallbackWrapper,\n retval: true\n });\n}\n\nfunction sendHttpError(request, response, code, text)\n{\n const corsHeaders = {\n 'access-control-allow-origin': '*',\n 'access-control-allow-headers': 'content-type',\n };\n\n response.setHeader('content-type', 'text/plain');\n response.setHeader('content-type', 'text/plain; charset=utf8');\n response.setHeader('cache-control', 'no-cache');\n for (let header in corsHeaders)\n response.setHeader(header, corsHeaders[header]);\n \n response.writeHead(code);\n response.write(`${code} - ${text}`);\n response.end();\n}\n\n/**\n * Utility function for use by the PseudoExpress class. Actually implements the \"routing\" behaviour.\n *\n * @param {object} descriptor mapping for the behaviour of the \"express\" rule - e.g., \n * send all GET queries at location /status to the status handler\n * @param {object} httpRequest the http request to handle, from the node http or https modules\n * @param {object} httpResponse the response object associated with httpRequest\n */\nfunction pseudoExpressRoute(descriptor, httpRequest, httpResponse)\n{\n const rMethod = httpRequest.method.toLowerCase();\n const request = new PseudoExpressRequest (httpRequest);\n const response = new PseudoExpressResponse(httpResponse, request);\n \n if (descriptor.method !== 'all' && descriptor.method !== rMethod)\n return; /* try next handler */\n\n try\n {\n if (rMethod === 'get')\n {\n let i = httpRequest.url.indexOf('?');\n request.query = {};\n \n if (i !== -1) /* query string present - decode */\n {\n let queryString = httpRequest.url.slice(i + 1);\n for (let kvp of queryString.split('&'))\n {\n let [ key, value ] = kvp.split('=');\n request.query[key] = value && decodeURIComponent(value.replace(/\\+/g, ' '));\n }\n }\n descriptor.callback(request, response);\n }\n else if (rMethod === 'post')\n {\n const chunks = [];\n request.on('data', chunk => chunks.push(chunk));\n request.on('end', function pseudoExpress$postEnd() {\n request.body = Buffer.concat(chunks);\n descriptor.callback(request, response);\n });\n }\n else\n {\n descriptor.callback(request, response);\n }\n }\n catch(error)\n {\n dumpError(request, response, error);\n }\n \n if (descriptor.hasOwnProperty('retval'))\n return descriptor.retval;\n}\n\n/**\n * @constructor\n * Class to represent incoming http request\n * @param {object} httpRequest NodeJS request from httpServer\n */\nfunction PseudoExpressRequest(httpRequest)\n{\n Object.assign(this, httpRequest);\n this.__httpRequest = httpRequest;\n this.originalUrl = this.__httpRequest.url;\n this.path = this.__httpRequest.url.replace(/\\?.*$/, '');\n this.hostname = this.__httpRequest.headers.host;\n}\n\n/**\n * @constructor\n * Class to represent outgoing http response\n */\nfunction PseudoExpressResponse(httpResponse, request)\n{\n this.httpResponse = httpResponse;\n this.headers = {};\n this.request = request;\n this.headersSent = false;\n}\n\n/**\n * @param {object} field Object containing multiple fields and values\n */\n/**\n * Sets the response’s HTTP header field to value. To set multiple fields at once, pass an \n * object as the parameter.\n * \n * @note Headers are collapsed to lower case\n *\n * @param {string} field Header name\n * @param {string} value Value of header\n */\nPseudoExpressResponse.prototype.set = function PseudoExpressResponse$$set(field, value)\n{\n if (typeof field === 'string') /* single header */\n this.headers[field.toLowerCase()] = value;\n else /* assume Object => multiple headers */\n {\n for (let entry of Object.values(field))\n this.headers[entry.field.toLowerCase()] = entry.value;\n }\n\n return this;\n}\n\n/** \n * Returns the HTTP response header specified by field. The match is case-insensitive.\n *\n * @param {string} field\n * @returns {string}\n */\nPseudoExpressResponse.prototype.get = function PseudoExpressResponse$$get(field)\n{\n return this.headers[field.toLowerCase()];\n}\n\n/**\n * Send HTTP status and headers immediately, without the respsonse body.\n *\n * @note Express does not appear to have an API that does this. It is needed to allow\n * streaming content.\n *\n * @param {object} defaultHeaders Headers to send as though they were in an object passed to\n * the set() method, unless they were already defined elsewhere\n * @param {object|string} body The body, per .send(), for the message; only used to detect\n * default header params; is not actually used in the response.\n * @returns this, to allow chaining\n */\nPseudoExpressResponse.prototype.sendHeaders = function PseudoExpressResponse$$sendHeaders(defaultHeaders, body)\n{\n var defaultContentType;\n\n if (!body)\n body = this.body;\n\n if (!this.headers['content-type'])\n {\n switch(typeof body)\n {\n case 'object':\n {\n if (Buffer.isBuffer(body))\n defaultContentType = 'application/octet-stream';\n else\n defaultContentType = 'application/json';\n break;\n }\n case 'string': \n defaultContentType = 'text/plain; charset=utf-8';\n break;\n default:\n defaultContentType = 'application/octet-stream';\n break;\n }\n }\n \n this.httpResponse.writeHead(this.statusCode || 200, Object.assign({ 'content-type': defaultContentType }, defaultHeaders, this.headers))\n this.headersSent = true;\n\n return this;\n}\n\n/**\n * Sends the HTTP response.\n * The body parameter can be a Buffer object, a String, an object. Plain objects are serialized via\n * JSON.stringify. This method performs many useful tasks for simple non-streaming responses: For \n * example, it automatically assigns the Content-Length HTTP response header field (unless previously \n * defined).\n * \n * When the parameter is a Buffer object, the method sets the Content-Type response header field \n * to “application/octet-stream”, unless previously defined.\n *\n * @param {string} body The body to send as the response.\n * @returns this, to allow chaining\n */\nPseudoExpressResponse.prototype.send = function PseudoExpressResponse$$send(body)\n{\n var defaultHeaders = {};\n var contentLength;\n var sendBody = body;\n var argv = Array.from(arguments);\n \n if (typeof sendBody === 'object' && sendBody !== null)\n {\n if (Buffer.isBuffer(sendBody))\n contentLength = Buffer.byteLength(sendBody);\n else\n sendBody = JSON.stringify(sendBody);\n }\n argv[0] = sendBody || '';\n\n if (typeof contentLength === 'undefined' && sendBody && typeof sendBody.length !== 'undefined')\n contentLength = sendBody.length;\n\n if (typeof contentLength !== 'undefined')\n defaultHeaders['content-length'] = contentLength;\n \n if (!this.headersSent)\n this.sendHeaders(defaultHeaders, body);\n\n try {\n this.httpResponse.end.apply(this.httpResponse, argv);\n }\n catch (error) {\n if (error.code === 'ERR_STREAM_WRITE_AFTER_END')\n console.warn('Pseudo-express tried to write after end. Suppressing here so we don\\'t crash');\n else {\n console.error('Unexpected error in pseudo-express', error);\n throw error;\n }\n\n }\n\n return this;\n}\n\n/**\n * Ends the response process. This method actually comes from Node core, specifically the \n * response.end() method of http.ServerResponse.\n *\n * Use to quickly end the response without any data. If you need to respond with data, instead \n * use methods such as res.send() and res.json().\n *\n * @param {string} [optional] _data\n * @param {string} [optional] _encoding\n *\n * @returns this, to allow chaining\n */\nPseudoExpressResponse.prototype.end = function PseudoExpressResponse$$end(_data, _encoding)\n{\n this.httpResponse.end.apply(this.httpResponse, arguments);\n\n return this;\n}\n\n/** \n * Sends a JSON response. This method sends a response (with the correct content-type) that is the \n * parameter converted to a JSON string using JSON.stringify().\n *\n * @param {object} bodyObject The object to send as the body of the response\n * @returns this, to allow chaining\n */\nPseudoExpressResponse.prototype.json = function PseudeoExpressResponse$$json(bodyObject)\n{\n this.send(JSON.stringify(bodyObject || typeof bodyObject));\n return this;\n}\n\n/**\n * Sets the HTTP status for the response. It is a chainable alias of Node’s response.statusCode.\n *\n * @param {number} statusCode\n * @returns this, to allow chaining\n */\nPseudoExpressResponse.prototype.status = function PseudoExpressResponse$$status(statusCode)\n{\n this.statusCode = statusCode;\n return this;\n}\n \n/**\n * Sets the response Location HTTP header to the specified path parameter.\n * A path value of “back” has a special meaning, it refers to the URL specified in the Referer\n * header of the request. If the Referer header was not specified, it refers to “/”.\n *\n * @param {string|URL|DcpURL} location\n * @returns this, to allow chaining\n */\nPseudoExpressResponse.prototype.location = function PseudoExpressResponse$$location(location)\n{\n if (location === '..')\n location = this.request.url.replace(/\\/[^/]*$/, '') || '/';\n if (location === 'back')\n location = this.request.headers['http-referer'] || '/';\n\n return this.set('location', location).send(`Redirecting to ${location}`);\n}\n\n/**\n * Sets the Content-Type HTTP header to the MIME type as determined by the specified type.\n *\n * If the type starts with 'text/' and the value does not contain a charset attribute, the attribute\n * charset=utf-8 will be added to the value.\n *\n * @param {string} contentType\n * @returns this, to allow chaining\n */\nPseudoExpressResponse.prototype.type = function PseudoExpressResponse$$type(contentType)\n{\n if (contentType.startsWith('text/') && contentType.includes('charset='))\n contentType += '; charset=\"utf-8\"';\n return this.set('content-type', contentType);\n}\n\n/**\n * Redirects to the URL derived from the specified path, with specified status, a positive integer\n * that corresponds to an HTTP status code. If not specified, status defaults to 302.\n *\n * @param {number} [optional] status\n * @param {string|URL|DcpURL} path\n * @returns this, to allow chaining\n */\nPseudoExpressResponse.prototype.redirect = function PseudoExpressResponse$$redirect(status, path)\n{\n if (typeof status !== 'number')\n {\n path = status;\n status = 302;\n }\n\n return this.status(status).location(path);\n}\n\n/* Try to tell user why pseudo-express \"route\" failed */\nfunction dumpError(request, response, error)\n{\n response.set('content-type', 'text/plain; charset=utf8');\n response.set('cache-control', 'no-cache');\n response.set('access-control-allow-origin', '*');\n response.set('access-control-allow-headers', 'content-type');\n response.statusCode = 500;\n response.write(`500 Internal Server Error accessing ${request.url} (${new Date()})\\n\\n` + error.stack);\n}\n\n\n//# sourceURL=webpack://dcp/./src/node-libs/pseudo-express.js?");
4492
4622
 
4493
4623
  /***/ }),
4494
4624
 
@@ -4628,7 +4758,7 @@ eval("/**\n * @file protocol/connection/response.js\n * @author Ryan
4628
4758
  /***/ ((module, __unused_webpack_exports, __webpack_require__) => {
4629
4759
 
4630
4760
  "use strict";
4631
- eval("/**\n * @file protocol/connection/sender.js\n * @author Ryan Rossiter, ryan@kingsds.network\n * @date January 2020\n *\n * The sender class is responsible for accepting Connection.Message instances,\n * and sending them to the peer via the provided transport instance.\n * Messages are queued in an array, and are sent in FIFO order - with the exception\n * of requests being skipped until there is a nonce available to send them with.\n */\n\n\nconst semver = __webpack_require__(/*! semver */ \"./node_modules/semver/semver.js\");\nconst DCP_ENV = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\nconst { assert } = __webpack_require__(/*! dcp/common/dcp-assert */ \"./src/common/dcp-assert.js\");\nconst { a$sleepMs } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\nconst debugging = (__webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope)('dcp');\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\nconst { setImmediate } = __webpack_require__(/*! dcp/common/dcp-timers */ \"./src/common/dcp-timers.js\");\nconst { role } = __webpack_require__(/*! ./connection-constants */ \"./src/protocol-v4/connection/connection-constants.js\");\n\nlet nanoid;\nif (DCP_ENV.platform === 'nodejs') {\n const { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\n nanoid = requireNative('nanoid').nanoid;\n} else {\n nanoid = (__webpack_require__(/*! nanoid */ \"./node_modules/nanoid/index.browser.js\").nanoid);\n}\n\nclass Sender {\n constructor(connection) {\n this.connection = connection;\n\n this.queue = [];\n this.servicingQueue = false;\n this.inFlight = null;\n this.nonce = null;\n this._ackTokenId = 0;\n \n this.debugLabel = this.connection.role === role.initiator ? `sender(i#${this.connection.id}):` : `sender(t#${this.connection.id}):`;\n }\n\n /**\n * We generate a unique token with each message we send so that \n * the peer can quickly ack the message and prove identity/uniqueness\n */\n makeAckToken() {\n return `${this.connection.id}-${this._ackTokenId++}-${nanoid()}`;\n }\n\n /**\n * Initiates the session by sending the 'connect' operation.\n * Once the response has been received, the session is established.\n * Lots of one-off code in here since it's difficult to re-use the \n * same methods meant for normal messages.\n * \n * @returns {Object} Session info\n */\n async establish() {\n assert(this.connection.transport);\n const halfSid = nanoid();\n const initiatorVersion = this.connection.constructor.VERSION;\n const connectRequest = new this.connection.Request('connect', {\n version: initiatorVersion,\n sid: halfSid,\n });\n connectRequest.id = `${this.connection.id}-connect-${nanoid()}`;\n debugging('sender') && console.debug(this.debugLabel, 'sending initial connect message');\n const resp = await this.specialFirstSend(connectRequest);\n if (typeof resp.payload !== 'object')\n throw new DCPError('Target responded to establish message without payload', 'DCPC-1018');\n if (resp.payload instanceof this.connection.ErrorPayload) {\n assert(resp.payload.type === 'protocol');\n throw new DCPError(resp.payload.message, resp.payload.code);\n }\n const targetVersion = resp.payload.version;\n const initiatorCompatibility = this.connection.constructor.VERSION_COMPATIBILITY;\n const versionCompatible = semver.satisfies(targetVersion, initiatorCompatibility);\n if (!versionCompatible) {\n debugging('sender') && console.log(this.debugLabel, `Target's version (${targetVersion}) is not compatible (must meet ${initiatorCompatibility})`);\n throw new DCPError(`Target's version (${targetVersion}) is not compatible (must meet ${initiatorCompatibility})`, 'DCPC-ETARGETVERSION');\n }\n\n // Memoize the peer version onto the Connection\n this.connection.peerVersion = targetVersion;\n if (this.connection.transport)\n this.connection.transport.peerVersion = targetVersion;\n\n // verify that our dcpsid half is still there, followed by a string\n // provided by the target that is at least the same length.\n // escape special chars:\n // eslint-disable-next-line no-useless-escape\n const regexSafeSid = halfSid.replace(/[-\\/\\\\^$*+?.()|[\\\\]{}]/g, '\\\\$&');\n const sidRegex = new RegExp(`^${regexSafeSid}.{${halfSid.length},}$`);\n if (typeof resp.dcpsid !== 'string' || !resp.dcpsid.match(sidRegex)) {\n throw new DCPError(`Target responded with invalid DCPSID: ${resp.dcpsid}`, 'DCPC-1008');\n }\n debugging('sender') && console.debug(this.debugLabel, 'connection established.');\n return {\n dcpsid: resp.dcpsid,\n peerAddress: resp.owner,\n };\n }\n\n /**\n * Invoked when the connection class becomes aware that a transport is available upon which\n * we can deliver traffic, this method either re-sends the current in-flight message, services\n * the queue if there are messages pending, or sends a keepalive to pump the queue if there's nothing\n * to send.\n */\n notifyTransportReady()\n {\n debugging('sender') && console.debug(this.debugLabel, `Notified transport is ready. inflight ${!!this.inFlight}, state ${this.connection.state}`)\n if (this.inFlight)\n this.sendInFlightMessage();\n else if (this.queue.length > 0)\n this.requestQueueService();\n else\n this.connection.keepalive().catch((error) => \n {\n if (error.code === 'ENODCPSID')\n {\n /* If our target crashed and restarted with no memory of previous connections, it will reject our message\n with the above code. Our connection will already be closing, so swallow and log this error. */\n debugging('connection') && console.debug(`${this.debugLabel} Target connection memo's corrupted. Keepalive rejected with ${error.code} `); \n }\n });\n }\n\n /**\n * We cannot use the normal enqueue logic for the first message\n * and we need many of the same bits of logic (but not all) so\n * this is the place for that kind of one-off logic.\n * We finish preparing the 'connect' request, put it in flight and send it out.\n * @param {Message} message \n */\n async specialFirstSend(message) { /* XXXwg - special first send *will* cause double dcpsid if invoked twice */\n message.ackToken = this.makeAckToken();\n let signedMessage = await message.sign();\n const messageSentPromise = this.connection.messageLedger.addMessage(message);\n \n this.inFlight = { message: message, signedMessage: signedMessage };\n if (message instanceof this.connection.Response) {\n this.connection.registerConnectResponse(this.inFlight.message); /* XXXwg todo - audit this.inFlight */\n }\n this.sendInFlightMessage();\n return messageSentPromise;\n }\n\n /**\n * Places message into `queue` but only schedules queue to be serviced\n * if connection is ready to send. Otherwise other tools will have to\n * handle making it ready to send again.\n * @param {Connection.Request|Connection.Response} message\n * @returns {Promise<Connection.Response>} from messageLedger that resolves when message response received.\n */\n enqueue(message) {\n if (message.payload && message.payload.operation)\n debugging('connection') && console.debug(this.debugLabel, `enqueueing ${message.type} message for operation:`, message.payload.operation);\n else\n debugging('connection') && console.debug(this.debugLabel, `enqueueing ${message.type} message`);\n \n this.queue.push(message);\n \n if (this.connection.state.in(['initial', 'disconnected']))\n this.connection.connect();\n\n setImmediate(() => this.requestQueueService());\n return this.connection.messageLedger.addMessage(message);\n }\n \n /**\n * Checks if we're able to send a message in our current state, and\n * services the queue if so.\n */\n requestQueueService()\n {\n if (this.inFlight || !this.nonce || this.servicingQueue)\n {\n debugging('sender') && debugging('verbose') && console.debug(\n `${this.debugLabel} request for queue service denied. inFlight: ${this.inFlight}, nonce: ${this.nonce}, queue running: ${this.servicingQueue}`\n )\n return;\n }\n else\n {\n debugging('sender') && console.debug(`${this.debugLabel} request for queue service accepted.`)\n this.serviceQueue();\n }\n }\n /**\n * Pulls a message from the queue. If the message is batchable, pauses for 30ms to allow other batchable\n * messages to enter the queue, and then continues dequeuing messages. \n * If the message is not batchable, ie. is here just to finish being prepared, then sign the message and emit\n * `message.id ready` to resolve the `Connection.prepareMessage` promise for the dequeued message.\n * When a message is signed, our nonce is deleted so that it may not be used in another message.\n */\n serviceQueue ()\n {\n if (!this.inFlight && this.connection.state.in(['established', 'closing', 'close-wait']))\n {\n debugging('sender') && console.debug(`${this.debugLabel} serviceQueue: checking queue`)\n this.servicingQueue = true;\n let nextMessage = this.queue.shift();\n if (!nextMessage)\n {\n debugging('sender') && console.debug(`${this.debugLabel} serviceQueue: nothing to send`)\n this.servicingQueue = false;\n return;\n }\n \n if (nextMessage.batchable)\n {\n debugging('sender') && console.debug(`${this.debugLabel} serviceQueue: pulled batchable message, allowing more messages to enter queue`)\n // Buffer the dequeuing of batchable messages to emulate the intention of Nagle's\n // algorithm, used to reduce the number of packets being sent over the network.\n a$sleepMs(this.connection.options.messageBatchingBuffer * 1000)\n .then(() =>\n {\n let rawBatch = [];\n do\n {\n rawBatch.push(nextMessage);\n nextMessage = this.queue.shift();\n } while (nextMessage && nextMessage.batchable && rawBatch.length < this.maxBatchSize)\n // if dequeuing a prepared message stopped the loop, put the message back in front \n if (nextMessage) this.queue.unshift(nextMessage);\n \n this.servicingQueue = false;\n debugging('sender') && console.debug(`${this.debugLabel} serviceQueue: pulled ${rawBatch.length} messages for batching. done servicing queue`)\n this.createAndSend(rawBatch);\n });\n }\n else\n {\n nextMessage.nonce = this.nonce;\n delete this.nonce;\n \n this.connection.emit(`${nextMessage.id} ready`, nextMessage);\n this.servicingQueue = false;\n debugging('sender') && console.debug(`${this.debugLabel} serviceQueue: pulled non-batchable message, finished its preparation. done servicing queue`)\n }\n \n }\n else\n {\n debugging('connection') && console.debug(this.debugLabel, `ignoring call to service queue. inFlight=${!!this.inFlight} state=${this.connection.state.valueOf()}`);\n }\n }\n\n /**\n * Takes an array of messages and batches them if there's more than one. Signs the resultant message and puts it in flight.\n * @param {Array<Connection.Message>} [messages] Array of messages to batch together for a transmission. \n * if not provided, draws from queue.\n *\n * @returns a Promise which resolves to a number which represents the number of messages sent. This is \n * either 0, 1, or the number of messages in a batch (possibly 1).\n */\n async createAndSend(messages) {\n assert(!this.inFlight);\n \n // By default, let `message` be first entry in input array.\n // If more than one entry in array, reassign `message` as batch object.\n let message = messages[0];\n if (messages.length > 1)\n message = new this.connection.Batch(messages, this.makeAckToken()); \n\n message.nonce = this.nonce;\n delete this.nonce;\n \n if (message instanceof this.connection.Batch) this.connection.messageLedger.addBatch(message);\n \n this.inFlight = { message: message };\n this.inFlight.signedMessage = await message.sign();\n \n debugging('connection') && console.debug(this.debugLabel, `sending a signed ${message.type} of ${messages.length}`);\n this.sendInFlightMessage();\n }\n \n /**\n * Sends the message stored in the `inFlight` var over the transport.\n * `clearFlightDeck` is the only method that should be resetting this.inFlight\n * and thus closing the loop.\n */\n sendInFlightMessage()\n {\n assert(this.inFlight);\n\n debugging('sender') && console.debug(this.debugLabel, `sending in-flight message ${this.inFlight.message.id}`);\n\n try\n {\n const message = this.inFlight.message;\n let type;\n \n /** XXXwg todo - figure out why Request/Response have name=Message and get rid of tests */\n if (message instanceof this.connection.Request)\n type = 'Request';\n else if (message instanceof this.connection.Response)\n type = 'Response';\n else \n type = message.constructor.name;\n\n if (!this.connection.transport)\n {\n debugging('sender') && console.debug(this.debugLabel, `no transport for ${type}; not sending`, this.inFlight.message.id,\n `(${this.inFlight.signedMessage.length} bytes)`);\n\n return;\n }\n \n \n debugging('sender') && console.debug(this.debugLabel, `sending ${type}`, this.inFlight.message.id,\n `(${this.inFlight.signedMessage.length} bytes)`);\n\n if (type === 'Batch')\n type += ':' + (this.inFlight.message.messages.length);\n debugging('wire') && console.debug(this.connection.debugLabel, `sendInFlightMessage: ${String(type).padEnd(10, ' ')} -> ${this.connection.loggableDest}`);\n this.connection.transport.send(this.inFlight.signedMessage);\n this.connection.emit('send', this.inFlight);\n }\n catch (error)\n {\n console.error(`Error while sending message ${this.inFlight.message.id} to ${this.connection.loggableDest}:`, error);\n debugging('sender') && console.debug(this.debugLabel, 'call stack:', new Error().stack);\n }\n }\n\n /**\n * Clear a message from the flight deck. For the foreseeable future the deck\n * only holds one message at a time, so we just assert that it matches.\n * @param {Connection.Message} message message that can be cleared from the flight deck\n */\n clearFlightDeck(message, nonce) {\n if (this.inFlight !== null) {\n debugging('sender') && console.debug(this.debugLabel, 'clearing flight deck. nonce =', nonce);\n assert(message === this.inFlight.message);\n this.inFlight = null;\n this.nonce = nonce;\n this.requestQueueService();\n }\n }\n\n /**\n * When a connection is closed the sender needs to cancel its send efforts.\n */\n shutdown() {\n debugging('sender') && console.debug(this.debugLabel, 'shutting down.');\n this.inFlight = null;\n this.nonce = null;\n this.queue = [];\n }\n\n // When allowBatch=false, set the batchSize to 1 so each\n // message gets sent individually (not in a batch)\n get maxBatchSize() {\n if (this._maxBatchSize) return this._maxBatchSize;\n const maxBatchSize = Math.max(this.connection.options.maxMessagesPerBatch, 1);\n this._maxBatchSize = this.connection.options.allowBatch ? maxBatchSize : 1;\n return this._maxBatchSize;\n }\n}\n\nObject.assign(module.exports, {\n Sender,\n});\n\n\n//# sourceURL=webpack://dcp/./src/protocol-v4/connection/sender.js?");
4761
+ eval("/**\n * @file protocol/connection/sender.js\n * @author Ryan Rossiter, ryan@kingsds.network\n * @date January 2020\n *\n * The sender class is responsible for accepting Connection.Message instances,\n * and sending them to the peer via the provided transport instance.\n * Messages are queued in an array, and are sent in FIFO order - with the exception\n * of requests being skipped until there is a nonce available to send them with.\n */\n\n\nconst semver = __webpack_require__(/*! semver */ \"./node_modules/semver/semver.js\");\nconst DCP_ENV = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\nconst { assert } = __webpack_require__(/*! dcp/common/dcp-assert */ \"./src/common/dcp-assert.js\");\nconst { a$sleepMs } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\nconst debugging = (__webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope)('dcp');\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\nconst { setImmediate } = __webpack_require__(/*! dcp/common/dcp-timers */ \"./src/common/dcp-timers.js\");\nconst { role } = __webpack_require__(/*! ./connection-constants */ \"./src/protocol-v4/connection/connection-constants.js\");\n\nlet nanoid;\nif (DCP_ENV.platform === 'nodejs') {\n const { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\n nanoid = requireNative('nanoid').nanoid;\n} else {\n nanoid = (__webpack_require__(/*! nanoid */ \"./node_modules/nanoid/index.browser.js\").nanoid);\n}\n\nclass Sender {\n constructor(connection) {\n this.connection = connection;\n\n this.queue = [];\n this.servicingQueue = false;\n this.inFlight = null;\n this.nonce = null;\n this._ackTokenId = 0;\n \n this.debugLabel = this.connection.role === role.initiator ? `sender(i#${this.connection.id}):` : `sender(t#${this.connection.id}):`;\n }\n\n /**\n * We generate a unique token with each message we send so that \n * the peer can quickly ack the message and prove identity/uniqueness\n */\n makeAckToken() {\n return `${this.connection.id}-${this._ackTokenId++}-${nanoid()}`;\n }\n\n /**\n * Initiates the session by sending the 'connect' operation.\n * Once the response has been received, the session is established.\n * Lots of one-off code in here since it's difficult to re-use the \n * same methods meant for normal messages.\n * \n * @returns {Object} Session info\n */\n async establish() {\n assert(this.connection.transport);\n const halfSid = nanoid();\n const initiatorVersion = this.connection.constructor.VERSION;\n const connectRequest = new this.connection.Request('connect', {\n version: initiatorVersion,\n sid: halfSid,\n });\n connectRequest.id = `${this.connection.id}-connect-${nanoid()}`;\n debugging('sender') && console.debug(this.debugLabel, 'sending initial connect message');\n const resp = await this.specialFirstSend(connectRequest);\n if (typeof resp.payload !== 'object')\n throw new DCPError('Target responded to establish message without payload', 'DCPC-1018');\n if (resp.payload instanceof this.connection.ErrorPayload) {\n assert(resp.payload.type === 'protocol');\n throw new DCPError(resp.payload.message, resp.payload.code);\n }\n const targetVersion = resp.payload.version;\n const initiatorCompatibility = this.connection.constructor.VERSION_COMPATIBILITY;\n const versionCompatible = semver.satisfies(targetVersion, initiatorCompatibility);\n if (!versionCompatible) {\n debugging('sender') && console.log(this.debugLabel, `Target's version (${targetVersion}) is not compatible (must meet ${initiatorCompatibility})`);\n throw new DCPError(`Target's version (${targetVersion}) is not compatible (must meet ${initiatorCompatibility})`, 'DCPC-ETARGETVERSION');\n }\n\n // Memoize the peer version onto the Connection\n this.connection.peerVersion = targetVersion;\n if (this.connection.transport)\n this.connection.transport.peerVersion = targetVersion;\n\n // verify that our dcpsid half is still there, followed by a string\n // provided by the target that is at least the same length.\n // escape special chars:\n // eslint-disable-next-line no-useless-escape\n const regexSafeSid = halfSid.replace(/[-\\/\\\\^$*+?.()|[\\\\]{}]/g, '\\\\$&');\n const sidRegex = new RegExp(`^${regexSafeSid}.{${halfSid.length},}$`);\n if (typeof resp.dcpsid !== 'string' || !resp.dcpsid.match(sidRegex)) {\n throw new DCPError(`Target responded with invalid DCPSID: ${resp.dcpsid}`, 'DCPC-1008');\n }\n debugging('sender') && console.debug(this.debugLabel, 'connection established.');\n return {\n dcpsid: resp.dcpsid,\n peerAddress: resp.owner,\n };\n }\n\n /**\n * Invoked when the connection class becomes aware that a transport is available upon which\n * we can deliver traffic, this method either re-sends the current in-flight message, services\n * the queue if there are messages pending, or sends a keepalive to pump the queue if there's nothing\n * to send.\n */\n notifyTransportReady()\n {\n debugging('sender') && console.debug(this.debugLabel, `Notified transport is ready. inflight ${!!this.inFlight}, state ${this.connection.state}`)\n if (this.inFlight)\n this.resendInFlightMessage();\n else if (this.queue.length > 0)\n this.requestQueueService();\n else\n this.connection.keepalive().catch((error) => \n {\n if (error.code === 'ENODCPSID')\n {\n /* If our target crashed and restarted with no memory of previous connections, it will reject our message\n with the above code. Our connection will already be closing, so swallow and log this error. */\n debugging('connection') && console.debug(`${this.debugLabel} Target connection memo's corrupted. Keepalive rejected with ${error.code} `); \n }\n });\n }\n\n /**\n * Invoked if we regain a transport connection while having a message in flight.\n * Ensures that we have a signed message to send before calling `sendInFlightMessage()`\n */\n async resendInFlightMessage()\n {\n if (this.inFlight.message && !this.inFlight.signedMessage)\n this.inFlight.signedMessage = await this.inFlight.message.sign();\n \n assert(this.inFlight.signedMessage);\n this.sendInFlightMessage();\n }\n \n /**\n * We cannot use the normal enqueue logic for the first message\n * and we need many of the same bits of logic (but not all) so\n * this is the place for that kind of one-off logic.\n * We finish preparing the 'connect' request, put it in flight and send it out.\n * @param {Message} message \n */\n async specialFirstSend(message) { /* XXXwg - special first send *will* cause double dcpsid if invoked twice */\n message.ackToken = this.makeAckToken();\n let signedMessage = await message.sign();\n const messageSentPromise = this.connection.messageLedger.addMessage(message);\n \n this.inFlight = { message: message, signedMessage: signedMessage };\n if (message instanceof this.connection.Response) {\n this.connection.registerConnectResponse(this.inFlight.message); /* XXXwg todo - audit this.inFlight */\n }\n this.sendInFlightMessage();\n return messageSentPromise;\n }\n\n /**\n * Places message into `queue` but only schedules queue to be serviced\n * if connection is ready to send. Otherwise other tools will have to\n * handle making it ready to send again.\n * @param {Connection.Request|Connection.Response} message\n * @returns {Promise<Connection.Response>} from messageLedger that resolves when message response received.\n */\n enqueue(message) {\n if (message.payload && message.payload.operation)\n debugging('connection') && console.debug(this.debugLabel, `enqueueing ${message.type} message for operation:`, message.payload.operation);\n else\n debugging('connection') && console.debug(this.debugLabel, `enqueueing ${message.type} message`);\n \n this.queue.push(message);\n \n if (this.connection.state.in(['initial', 'disconnected']))\n this.connection.connect();\n\n setImmediate(() => this.requestQueueService());\n return this.connection.messageLedger.addMessage(message);\n }\n \n /**\n * Checks if we're able to send a message in our current state, and\n * services the queue if so.\n */\n requestQueueService()\n {\n if (this.inFlight || !this.nonce || this.servicingQueue)\n {\n debugging('sender') && debugging('verbose') && console.debug(\n `${this.debugLabel} request for queue service denied.`,\n `inFlight: ${this.inFlight && this.inFlight.message && this.inFlight.message.id},`,\n `nonce: ${this.nonce}, queue running: ${this.servicingQueue}`\n );\n return;\n }\n else\n {\n debugging('sender') && console.debug(`${this.debugLabel} request for queue service accepted.`)\n this.serviceQueue();\n }\n }\n /**\n * Pulls a message from the queue. If the message is batchable, pauses for 30ms to allow other batchable\n * messages to enter the queue, and then continues dequeuing messages. \n * If the message is not batchable, ie. is here just to finish being prepared, then sign the message and emit\n * `message.id ready` to resolve the `Connection.prepareMessage` promise for the dequeued message.\n * When a message is signed, our nonce is deleted so that it may not be used in another message.\n */\n serviceQueue ()\n {\n if (!this.inFlight && this.connection.state.in(['established', 'closing', 'close-wait']))\n {\n debugging('sender') && console.debug(`${this.debugLabel} serviceQueue: checking queue`)\n this.servicingQueue = true;\n let nextMessage = this.queue.shift();\n if (!nextMessage)\n {\n debugging('sender') && console.debug(`${this.debugLabel} serviceQueue: nothing to send`)\n this.servicingQueue = false;\n return;\n }\n \n if (nextMessage.batchable)\n {\n debugging('sender') && console.debug(`${this.debugLabel} serviceQueue: pulled batchable message, allowing more messages to enter queue`)\n // Buffer the dequeuing of batchable messages to emulate the intention of Nagle's\n // algorithm, used to reduce the number of packets being sent over the network.\n a$sleepMs(this.connection.options.messageBatchingBuffer * 1000)\n .then(() =>\n {\n let rawBatch = [];\n do\n {\n rawBatch.push(nextMessage);\n nextMessage = this.queue.shift();\n } while (nextMessage && nextMessage.batchable && rawBatch.length < this.maxBatchSize)\n // if dequeuing a prepared message stopped the loop, put the message back in front \n if (nextMessage) this.queue.unshift(nextMessage);\n \n this.servicingQueue = false;\n debugging('sender') && console.debug(`${this.debugLabel} serviceQueue: pulled ${rawBatch.length} messages for batching. done servicing queue`)\n this.createAndSend(rawBatch);\n });\n }\n else\n {\n nextMessage.nonce = this.nonce;\n delete this.nonce;\n \n this.connection.emit(`${nextMessage.id} ready`, nextMessage);\n this.servicingQueue = false;\n debugging('sender') && console.debug(`${this.debugLabel} serviceQueue: pulled non-batchable message, finished its preparation. done servicing queue`)\n }\n \n }\n else\n {\n debugging('connection') && console.debug(this.debugLabel, `ignoring call to service queue. inFlight=${!!this.inFlight} state=${this.connection.state.valueOf()}`);\n }\n }\n\n /**\n * Takes an array of messages and batches them if there's more than one. Signs the resultant message and puts it in flight.\n * @param {Array<Connection.Message>} [messages] Array of messages to batch together for a transmission. \n * if not provided, draws from queue.\n *\n * @returns a Promise which resolves to a number which represents the number of messages sent. This is \n * either 0, 1, or the number of messages in a batch (possibly 1).\n */\n async createAndSend(messages) {\n assert(!this.inFlight);\n \n // By default, let `message` be first entry in input array.\n // If more than one entry in array, reassign `message` as batch object.\n let message = messages[0];\n if (messages.length > 1)\n message = new this.connection.Batch(messages, this.makeAckToken()); \n\n message.nonce = this.nonce;\n delete this.nonce;\n \n if (message instanceof this.connection.Batch) this.connection.messageLedger.addBatch(message);\n \n this.inFlight = { message: message };\n this.inFlight.signedMessage = await message.sign();\n \n debugging('connection') && console.debug(this.debugLabel, `sending a signed ${message.type} of ${messages.length}`);\n this.sendInFlightMessage();\n }\n \n /**\n * Sends the message stored in the `inFlight` var over the transport.\n * `clearFlightDeck` is the only method that should be resetting this.inFlight\n * and thus closing the loop.\n */\n sendInFlightMessage()\n {\n assert(this.inFlight);\n\n debugging('sender') && console.debug(this.debugLabel, `sending in-flight message ${this.inFlight.message.id}`);\n\n try\n {\n const message = this.inFlight.message;\n let type;\n \n /** XXXwg todo - figure out why Request/Response have name=Message and get rid of tests */\n if (message instanceof this.connection.Request)\n type = 'Request';\n else if (message instanceof this.connection.Response)\n type = 'Response';\n else \n type = message.constructor.name;\n\n if (!this.connection.transport)\n {\n debugging('sender') && console.debug(this.debugLabel, `no transport for ${type}; not sending`, this.inFlight.message.id,\n `(${this.inFlight.signedMessage.length} bytes)`);\n\n return;\n }\n \n \n debugging('sender') && console.debug(this.debugLabel, `sending ${type}`, this.inFlight.message.id,\n `(${this.inFlight.signedMessage.length} bytes)`);\n\n if (type === 'Batch')\n type += ':' + (this.inFlight.message.messages.length);\n debugging('wire') && console.debug(this.connection.debugLabel, `sendInFlightMessage: ${String(type).padEnd(10, ' ')} -> ${this.connection.loggableDest}`);\n this.connection.transport.send(this.inFlight.signedMessage);\n this.connection.emit('send', this.inFlight);\n }\n catch (error)\n {\n console.error(`Error while sending message ${this.inFlight.message.id} to ${this.connection.loggableDest}:`, error);\n debugging('sender') && console.debug(this.debugLabel, 'call stack:', new Error().stack);\n }\n }\n\n /**\n * Clear a message from the flight deck. For the foreseeable future the deck\n * only holds one message at a time, so we just assert that it matches.\n * @param {Connection.Message} message message that can be cleared from the flight deck\n */\n clearFlightDeck(message, nonce) {\n if (this.inFlight !== null) {\n debugging('sender') && console.debug(this.debugLabel, 'clearing flight deck. nonce =', nonce);\n assert(message === this.inFlight.message);\n this.inFlight = null;\n this.nonce = nonce;\n this.requestQueueService();\n }\n }\n\n /**\n * When a connection is closed the sender needs to cancel its send efforts.\n */\n shutdown() {\n debugging('sender') && console.debug(this.debugLabel, 'shutting down.');\n this.inFlight = null;\n this.nonce = null;\n this.queue = [];\n }\n\n // When allowBatch=false, set the batchSize to 1 so each\n // message gets sent individually (not in a batch)\n get maxBatchSize() {\n if (this._maxBatchSize) return this._maxBatchSize;\n const maxBatchSize = Math.max(this.connection.options.maxMessagesPerBatch, 1);\n this._maxBatchSize = this.connection.options.allowBatch ? maxBatchSize : 1;\n return this._maxBatchSize;\n }\n}\n\nObject.assign(module.exports, {\n Sender,\n});\n\n\n//# sourceURL=webpack://dcp/./src/protocol-v4/connection/sender.js?");
4632
4762
 
4633
4763
  /***/ }),
4634
4764
 
@@ -4682,7 +4812,7 @@ eval("/**\n * @file connection/target.js\n * @author Wes Garland, we
4682
4812
  /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
4683
4813
 
4684
4814
  "use strict";
4685
- eval("/**\n * @file http-bearer.js\n * Utility code for transports working with http (or https) bearers\n *\n * @author Wes Garland, wes@kingsds.network\n * @date Apr 2022\n */\n\nconst { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\nconst debugging = (__webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope)('dcp');\n\n/**\n * Returns true if friendLocation should work in place of location from this host.\n * This allows us to transparently configure inter-daemon communication that uses\n * local LAN IPs instead of bouncing off the firewall for NAT.\n */\nexports.a$isFriendlyUrl = async function httpBearer$$isFriendlyUrl(url)\n{\n var remoteIp, dnsA;\n var ifaces;\n \n if (!url)\n return false;\n \n if (url.hostname === 'localhost')\n return true;\n\n switch(url.protocol)\n {\n case 'http:':\n case 'https:':\n case 'ws:':\n case 'tcp:':\n case 'udp:':\n case 'dcpsaw:':\n break;\n default:\n return false;\n }\n\n /* Consider same-origin match friendly */\n if ((__webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\").isBrowserPlatform))\n return url.origin === window.location.origin;\n\n /* Convert an IP address to a 32-bit int in network order */\n function i32(addr)\n {\n var ret = 0;\n var octets = addr.split('.');\n\n ret |= octets[0] << 24; /* Note: JS ints are signed 32, but that doesn't matter for masking */\n ret |= octets[1] << 16;\n ret |= octets[2] << 8;\n ret |= octets[3] << 0;\n\n return ret;\n }\n \n /* Consider machines in same IPv4 subnet friendly */\n dnsA = await requireNative('dns').promises.lookup(url.hostname, { family: 4 });\n if (!dnsA)\n return false;\n remoteIp = i32(dnsA.address);\n ifaces = requireNative('os').networkInterfaces();\n for (let ifaceName of Object.keys(ifaces))\n {\n for (let alias of ifaces[ifaceName])\n {\n if (alias.family !== 'IPv4')\n continue;\n\n let i32_addr = i32(alias.address);\n let i32_mask = i32(alias.netmask);\n\n if ((i32_addr & i32_mask) === (remoteIp & i32_mask))\n return true;\n }\n }\n\n return false;\n}\n\n/**\n * Factory which create instances of Server for targets/listeners, suitable for binding to\n * transports which use an underlying http (or https) bearer.\n *\n * @param {object} config the configuration object for the listener; must have\n * either a location (or ideally) a listen property which\n * is an instance of URL or DcpURL.\n * @param {function} listeningHandler [optional] a function which is invoked as soon as the\n * underlying server is listening for new connections. It\n * receives as its sole argument an instance of Server.\n */\nexports.httpServerFactory = function(config, listeningHandler)\n{\n var bearer;\n var listen = config.listen || config.location;\n\n const listenOpts = {\n host: listen.hostname,\n port: listen.port\n };\n\n switch (listen.protocol)\n {\n case 'http:':\n bearer = requireNative('http');\n listenOpts.port = listenOpts.port || 80;\n break;\n case 'https:':\n bearer = requireNative('https');\n listenOpts.port = listenOpts.port || 443;\n break;\n default:\n throw new Error(`unknown bearer: '${listen.protocol}'`)\n }\n\n if (listenOpts.host === 'inaddr_any' || !listenOpts.host)\n delete listenOpts.host;\n \n const server = bearer.createServer();\n server.listen(listenOpts, () => {\n debugging() && console.debug('http server listening on', (listenOpts.host || 'any/0') + ':' + listenOpts.port);\n listeningHandler(server);\n });\n\n return server;\n}\n\n\n//# sourceURL=webpack://dcp/./src/protocol-v4/transport/http-bearer.js?");
4815
+ eval("/**\n * @file http-bearer.js\n * Utility code for transports working with http (or https) bearers\n *\n * @author Wes Garland, wes@kingsds.network\n * @date Apr 2022\n */\n\nconst { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\nconst debugging = (__webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope)('dcp');\n\n/**\n * Returns true if friendLocation should work in place of location from this host.\n * This allows us to transparently configure inter-daemon communication that uses\n * local LAN IPs instead of bouncing off the firewall for NAT.\n */\nexports.a$isFriendlyUrl = async function httpBearer$$isFriendlyUrl(url)\n{\n var remoteIp, dnsA;\n var ifaces;\n \n if (!url)\n return false;\n \n if (url.hostname === 'localhost')\n return true;\n\n switch(url.protocol)\n {\n case 'http:':\n case 'https:':\n case 'ws:':\n case 'tcp:':\n case 'udp:':\n case 'dcpsaw:':\n break;\n default:\n return false;\n }\n\n /* Consider same-origin match friendly */\n if ((__webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\").isBrowserPlatform))\n return url.origin === window.location.origin;\n\n /* Convert an IP address to a 32-bit int in network order */\n function i32(addr)\n {\n var ret = 0;\n var octets = addr.split('.');\n\n ret |= octets[0] << 24; /* Note: JS ints are signed 32, but that doesn't matter for masking */\n ret |= octets[1] << 16;\n ret |= octets[2] << 8;\n ret |= octets[3] << 0;\n\n return ret;\n }\n \n /* Consider machines in same IPv4 subnet friendly */\n dnsA = await requireNative('dns').promises.lookup(url.hostname, { family: 4 });\n if (!dnsA)\n return false;\n remoteIp = i32(dnsA.address);\n ifaces = requireNative('os').networkInterfaces();\n for (let ifaceName of Object.keys(ifaces))\n {\n for (let alias of ifaces[ifaceName])\n {\n if (alias.family !== 'IPv4')\n continue;\n\n let i32_addr = i32(alias.address);\n let i32_mask = i32(alias.netmask);\n\n if ((i32_addr & i32_mask) === (remoteIp & i32_mask))\n return true;\n }\n }\n\n return false;\n}\n\n/**\n * Factory which create instances of Server for targets/listeners, suitable for binding to\n * transports which use an underlying http (or https) bearer.\n *\n * @param {object} config the configuration object for the listener; must have\n * either a location (or ideally) a listen property which\n * is an instance of URL or DcpURL.\n * @param {function} listeningHandler [optional] a function which is invoked as soon as the\n * underlying server is listening for new connections. It\n * receives as its sole argument an instance of Server.\n */\nexports.httpServerFactory = function(config, listeningHandler)\n{\n var bearer;\n var listen = config.listen || config.location;\n\n const listenOpts = {\n host: listen.hostname,\n port: listen.port\n };\n\n switch (listen.protocol)\n {\n case 'http:':\n bearer = requireNative('http');\n listenOpts.port = listenOpts.port || 80;\n break;\n case 'https:':\n bearer = requireNative('https');\n listenOpts.port = listenOpts.port || 443;\n break;\n default:\n throw new Error(`unknown bearer: '${listen.protocol}'`)\n }\n\n if (listenOpts.host === 'inaddr_any' || !listenOpts.host)\n delete listenOpts.host;\n if (listenOpts.host === 'localhost')\n listenOpts.host = '::'; /* transparently support IPv4 and IPv6 link-local addresses */\n \n const server = bearer.createServer();\n server.listen(listenOpts, () => {\n debugging() && console.debug('http server listening on', (listenOpts.host || 'any/0') + ':' + listenOpts.port);\n listeningHandler(server);\n });\n\n return server;\n}\n\n\n//# sourceURL=webpack://dcp/./src/protocol-v4/transport/http-bearer.js?");
4686
4816
 
4687
4817
  /***/ }),
4688
4818
 
@@ -4715,7 +4845,7 @@ eval("/**\n * @file listener.js\n * Generic API for transpor
4715
4845
  /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
4716
4846
 
4717
4847
  "use strict";
4718
- eval("/* provided dependency */ var process = __webpack_require__(/*! ./node_modules/process/browser.js */ \"./node_modules/process/browser.js\");\n/**\n * @file transport/socketio.js\n * @author Ryan Rossiter, ryan@kingsds.network\n * @author Wes Garland, wes@kingsds.network \n * @date January 2020, March 2022\n *\n * This module implements the SocketIO Transport that is\n * used by the protocol to connect and communicate with peers using\n * SocketIO connections.\n *\n * Transport can operate in either ack mode or non-ack-mode, depending on the value of this.ackMode.\n * Ack mode sends an extra socketio-layer packet back after each message is received, and theoretically\n * this can be use tod meter or measure bandwidth or potentially second-guess socketio's ability to\n * interleave its heartbeat messages OOB from the main traffic. This mode could potentially be enabled\n * on-demand as well, although it appears that it is not necessary to get the desired behaviour at this\n * point, so the feature is off-by-default (although reasonably well-tested).\n */\n\n\nconst { Transport } = __webpack_require__(/*! . */ \"./src/protocol-v4/transport/index.js\");\nconst { EventEmitter } = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\");\nconst { leafMerge } = __webpack_require__(/*! dcp/utils/obj-merge */ \"./src/utils/obj-merge.js\");\nconst { DcpURL } = __webpack_require__(/*! dcp/common/dcp-url */ \"./src/common/dcp-url.js\");\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\nconst { setImmediateN } = __webpack_require__(/*! dcp/common/dcp-timers */ \"./src/common/dcp-timers.js\");\nconst { setImmediate } = __webpack_require__(/*! dcp/common/dcp-timers */ \"./src/common/dcp-timers.js\");\nconst { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\nconst { assert } = __webpack_require__(/*! dcp/common/dcp-assert */ \"./src/common/dcp-assert.js\");\n\nconst debugging = (__webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope)('dcp');\nconst dcpEnv = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\nconst bearer = __webpack_require__(/*! ./http-bearer */ \"./src/protocol-v4/transport/http-bearer.js\");\nconst semver = __webpack_require__(/*! semver */ \"./node_modules/semver/semver.js\");\n\nvar listenerSeq = 0;\nvar initiatorSeq = 0;\n\nif (debugging() && !process.env.DEBUG && dcpConfig.build === 'debug')\n{\n if (process.env.DEBUG)\n process.env.DEBUG += ',engine';\n else\n process.env.DEBUG = 'engine';\n\n if (debugging('socketio'))\n {\n /* DCP_DEBUG=dcp:socketio,dcp:verbose to see everything */\n process.env.DEBUG += ',socket.io-client:socket,socket.io:client,socket.io:server';\n if (debugging('verbose') && !debugging('all'))\n process.env.DEBUG += ',socket.io*,engine.io*'\n }\n}\n\nclass SocketIOTransport extends Transport\n{\n /**\n * @constructor\n * @param {socket} newSocket Target: an instance of socket.io Socket that represents\n * a new connection.\n * Initiator: unused\n */\n /**\n * @constructor create new instance of a socketio-flavoured Transport.\n *\n * @param {object} config dcpConfig fragment\n * @param {Url} url Initiator: has .location and optionally .friendLocation\n * Target: has .listen and .location\n *\n * @param {object} options The `socketio` property of a protocol-v4 connectionOptions \n * object, which was built from a combination of defaults and \n * various dcpConfig components.\n *\n * @returns instance suitable for target or initiator, depending on mode, but does not manage connections.\n */\n constructor(config, options={}, newSocket)\n {\n super('Protocol SocketIO Transport');\n const that = this;\n \n this.name = 'socketio';\n this.url = config.location;\n this.msgSequence = 0;\n this.isClosed = false; /* true => connection finalization begun */\n this.isFinal = false; /* true => connection finalization completed */\n this.hasConnected = false; /* true => one 'connect' event has been processed */\n this.initTimeout = false; /* timeout when connecting to allow target to reply with their mode message + buffer size, crash if they don't reply */\n this.modeSet = false; /* true => received response mode message */\n this.bufferSizeSet = false; /* true => received target's buffer size */\n this.rxFragments = {}; /* messages we are receiving in fragments */\n this.rxPending = []; /* strings which are messages that are complete and ready to emit, or objects that in this.rxFragments */\n this.txPending = []; /* messages we are receiving in fragments */\n this.txNumInFlight = 0; /* number of txPending sent but not acknowleged */\n this.txMaxInFlight = 2; /* less than 2 causes bad perf when mode.ack */\n this.debugInfo = { remoteLabel: '<unknown>' };\n this.mode = {\n ack: false,\n concat: false,\n };\n\n assert(DcpURL.isURL(this.url));\n assert(typeof options === 'object');\n\n this.options = this.buildOptions(options);\n\n if (!newSocket) /* Initiator */\n {\n this.debugLabel = `socketio(i:${++initiatorSeq}):`;\n this.rxReady = true; /* relax, said the night man */\n this.debugInfo.remoteLabel = config.location.href;\n if (this.options.disableModeAnnouncement !== false)\n this.sendModeList();\n this.sendMaxMessageInfo();\n\n bearer.a$isFriendlyUrl(config.friendLocation).then((useFriendLocation) => {\n const connectUrl = useFriendLocation ? config.friendLocation : config.location;\n\n debugging('socketio') && console.log(this.debugLabel, 'connecting to', connectUrl.href);\n this.socket = (__webpack_require__(/*! socket.io-client */ \"./node_modules/socket.io-client/build/cjs/index.js\").connect)(connectUrl.origin, this.options);\n this.socket.on('connect_error', (error) => !this.isClosed && this.handleConnectErrorEvent(error));\n this.socket.on('connect', () => !this.isClosed && this.handleConnectEvent());\n\n useSocket();\n }).catch((error) => {\n debugging() && console.error(this.debugLabel, error);\n this.emit('error', error);\n });\n }\n else /* Target - receives socketio instance from SocketIOListener.handleConnectionEvent */\n {\n if (dcpEnv.platform !== 'nodejs')\n throw new Error('socketio: target mode only supported in nodejs');\n\n this.debugLabel = 'socketio(t:<new>):';\n\n this.socket = newSocket;\n this.debugInfo.remoteLabel = this.socket.handshake.address.replace(/^::ffff:/,'');\n debugging('socketio') && console.debug(this.debugLabel, `new socket from ${this.debugInfo.remoteLabel}`\n + ` on ${this.socket.handshake && this.socket.handshake.url}, ${this.socket.id}`);\n\n useSocket();\n }\n\n function useSocket()\n {\n that.socket.compress(dcpConfig.build !== 'debug'); /* try to keep traffic sniffable in debug */\n that.socket.on('message', (msg) => !that.isFinal && that.handleMessageEvent(msg));\n that.socket.on('disconnect', (reason) => !that.isClosed && that.handleDisconnectEvent(reason));\n }\n }\n\n /**\n * API - determine if a transport is usable or not.\n * @returns true if the transport is not closed.\n */\n ready()\n {\n return !this.isClosed;\n }\n \n /**\n * Handle the socket.io disconnect event, which is fired upon disconnection.\n * For some reasons (explicit disconnection), the socket.io code will not try to reconnect, but\n * in all other cases, the socket.io client will normally wait for a small random delay and then \n * try to reconnect, but in our case, we simply tear the transport down and let the Connection\n * class handle reconnects, since it might prefer a different transport at this point anyhow.\n *\n * One tricky case here is ping timeout, since the ping timeout includes the time to transmit the\n * packet before it was sent.\n *\n * @param {string} reason Possible reasons for disconnection.\n */\n handleDisconnectEvent(reason)\n {\n debugging('socketio') && console.debug(this.debugLabel, `disconnected from ${this.debugInfo.remoteLabel}; reason=${reason}`);\n\n if (this.isClosed !== true)\n setImmediate(() => this.#close(reason));\n\n switch(reason)\n {\n case 'io client disconnect':\n return; /* we called socket.disconnect() from another \"thread\" */\n case 'io server disconnect':\n case 'ping timeout':\n case 'transport close':\n case 'transport error':\n this.emit('end', reason);\n /* fallthrough */\n default:\n }\n }\n\n /**\n * Handle a socketio message from the peer.\n *\n * Most of the time, a socketio message contains a DCP message, however we support other message\n * types as well, with a mechanism inspired by 3GPP TS 23.040 (GSM 03.40) message concatenation.\n */\n handleMessageEvent(msg)\n {\n if (typeof msg !== 'string')\n {\n debugging('socketio') && console.debug(this.debugLabel, `received ${typeof msg} message from peer`, this.debugInfo.remoteLabel);\n return;\n }\n\n try\n {\n switch (msg[0])\n {\n case '{': /* all DCP Messages are JSON */\n this.processMessage(msg);\n break;\n case 'E':\n this.processExtendedMessage(msg);\n break;\n case 'A':\n this.processAcknowledgement(msg);\n break;\n case 'T':\n debugging('socketio') && console.debug('Received debug message:', msg);\n break;\n default:\n /* adhoc messages not supported in this transport */\n throw new DCPError(this.debugLabel, `Unrecognized message type indicator from ${this.debugInfo.remoteLabel} (${msg.charCodeAt(0)})`, 'DCPC-1102');\n }\n }\n catch(error)\n {\n debugging('socketio') && console.debug(this.debugLabel, 'handleMessageEvent: transport error, closing\\n ', error);\n this.emit('error', error);\n setImmediate(() => this.#close(error.message));\n }\n }\n\n /**\n * Process an ordinary message. This is just a hunk of JSON that gets passed up to the connection.\n * @param {string} msg\n */\n processMessage(msg)\n {\n this.rxPending.push(msg);\n this.emitReadyMessages();\n if (this.mode.ack)\n this.socket.send('A0000-ACK'); /* Ack w/o header = normal message */\n }\n\n /**\n * Remote has acknowledged the receipt of a message fragment. When in ack mode, this\n * triggers us to send more messages from the txPending queue. We always ack event when \n * not in ack-mode. See processExtendedMessage() comment for header description.\n *\n * Future: this is where maxInFlight might be tuned, based on how fast we get here\n *\n * @param {string} _msg\n */\n processAcknowledgement(_msg)\n {\n if (this.txNumInFlight > 0)\n this.txNumInFlight--;\n else\n {\n if (this.options.strict || !this.mode.ack)\n throw new DCPError(this.debugLabel, `Synchronization error: received unexpected acknowledgement message from ${this.debugInfo.remoteLabel}`, 'DCPC-1109');\n else\n this.mode.ack = true;\n }\n \n this.drainTxPending_soon();\n }\n\n /**\n * Extended messages have the following format:\n * Octet(s) Description\n * 0 E (69) or A (65)\n * 1..4 Header size (N) in hexadecimal\n * 5 Header Type\n * C (67) => concatenation\n * 6..6 + N: Header\n *\n * Upon receipt, extended messages are acknowledged by retransmitting their header, but\n * with the first character changed from E to A.\n *\n * @param {string} msg\n */\n processExtendedMessage(msg)\n {\n const headerSize = parseInt(msg.slice(1,5), 16);\n const headerType = msg[5];\n const header = msg.slice(6, 6 + headerSize);\n\n switch(headerType)\n {\n case 'M':\n this.processExtendedMessage_mode(header, msg.slice(6 + headerSize));\n break;\n case 'B':\n this.processExtendedMessage_maxBufferSize(msg.slice(6 + headerSize));\n break;\n case 'C':\n this.processExtendedMessage_concatenated(header, msg.slice(6 + headerSize));\n break;\n default:\n if (this.options.strict)\n throw new DCPError(`Unrecognized extended message header type indicator (${msg.charCodeAt(5)})`, 'DCPC-1101');\n }\n\n if (this.mode.ack)\n this.socket.send('A' + msg.slice(1, 6 + headerSize));\n }\n\n /**\n * We have received a fragment of a concatenation extended message. Memoize the fragment, and\n * if this is the last fragment, transform the fragments list into a pending message string and\n * then emits all ready messages (pending message strings).\n *\n * This code is capable of handling messages whose fragments arrive out of order. Complete messages \n * are emitted in the order that their first parts are received. Socketio is supposed to be total\n * order-aware but I wanted a safety belt for intermingled messages without occupying the entire\n * socketio network buffer on the first pass of the event loop. Because the first message fragment\n * is emitted to socketio on the same event loop pass as their Connection::send calls, I believe\n * that are no cases where messages will be emitted out of order at the other end, even when \n * intermingled on the wire, and the Nth message is much longer than any (N+k)th messages.\n *\n * @param {object} header the extended message header\n * @param {string} payload the payload of the extended message, i.e. a message fragment\n */\n processExtendedMessage_concatenated(header, payload)\n {\n try\n {\n header = JSON.parse(header);\n }\n catch(e)\n {\n throw new DCPError(`invalid extended message header '${header}'`, 'DCPC-1103');\n }\n\n if (!this.options.strict && !this.mode.concat)\n this.mode.concat = true;\n \n if (!(header.total <= this.options.maxFragmentCount))\n throw new DCPError('excessive message fragmentation', 'DCPC-1107'); /* make it more difficult for attacker to force OOM us */\n\n if (!(header.seq < header.total))\n throw new DCPError(`corrupt header for part ${header.seq + 1} of ${header.total} of message ${header.msgId}`, 'DCPC-1108');\n \n /* First fragment received, initial data structures and memoize the message's arrival order via pending list */\n if (!this.rxFragments[header.msgId])\n {\n const fragments = [];\n this.rxFragments[header.msgId] = fragments;\n this.rxPending.push(fragments);\n }\n\n this.rxFragments[header.msgId][header.seq] = payload;\n debugging('socketio') && console.debug(this.debugLabel, 'received part', header.seq + 1, 'of', header.total, 'for message', header.msgId);\n\n if (header.total !== this.rxFragments[header.msgId].filter(el => el !== undefined).length)\n return;\n\n debugging('socketio') && console.debug(this.debugLabel, 'received all parts of message', header.msgId);\n \n const idx = this.rxPending.indexOf(this.rxFragments[header.msgId])\n this.rxPending[idx] = this.rxFragments[header.msgId].join('');\n delete this.rxFragments[header.msgId];\n this.emitReadyMessages();\n }\n\n processExtendedMessage_mode(_header, payload)\n {\n var modeList;\n \n try\n {\n modeList = JSON.parse(payload);\n }\n catch(e)\n {\n throw new DCPError(`invalid mode message payload '${payload}'`, 'DCPC-1105');\n }\n\n for (let mode of modeList)\n {\n debugging('socketio') && console.debug(this.debugLabel, ` - remote supports ${mode} mode`); \n switch(mode)\n {\n case 'ack':\n this.mode.ack = true;\n break;\n case 'concat':\n this.mode.concat = true;\n break;\n default:\n debugging('socketio') && console.debug(this.debugLabel, ' - remote requested unrecognized mode:', mode);\n break;\n }\n }\n\n // true if we are connecting a new transport, expecting the initial mode reply, and have received the target's buffer size\n if (this.initTimeout && this.bufferSizeSet)\n {\n clearTimeout(this.initTimeout);\n delete this.initTimeout;\n this.emit('connect');\n }\n this.modeSet = true;\n this.sendModeList();\n }\n \n processExtendedMessage_maxBufferSize(payload)\n {\n var receivedOptions = JSON.parse(payload); \n \n this.options.txMaxFragmentSize = receivedOptions.maxFragmentSize;\n this.options.txMaxFragmentCount = receivedOptions.maxFragmentCount;\n \n if (receivedOptions.txMaxInFlight < this.txMaxInFlight)\n this.txMaxInFlight = receivedOptions.txMaxInFlight;\n if (this.options.maxFragmentSize < 0)\n throw new DCPError(`maxFragmentSize was set to < 1, (${this.options.txMaxFragmentSize}). Server requested bad fragment size, cannot connect.`, 'DCPC-1111');\n if (this.txMaxInFlight < 1)\n throw new DCPError(`txMaxInFlight was set to < 1, (${this.txMaxInFlight}). Cannot send messages with fewer than 1 in flight.`, 'DCPC-1111');\n \n // true if we are connecting a new transport, expecting the initial buffer reply, and have received the target's mode designation\n if (this.initTimeout && this.modeSet)\n {\n clearTimeout(this.initTimeout);\n delete this.initTimeout;\n this.emit('connect');\n }\n this.bufferSizeSet = true;\n\n this.sendMaxMessageInfo(); \n }\n\n /* Internal method to tell peer what we can receive */\n sendModeList()\n {\n var rawMessage;\n var modeList = [];\n\n if (this.sentModeList)\n return;\n\n this.sentModeList = true;\n this.options.enableModeAck == true && modeList.push('ack');\n this.options.enableModeConcat !== false && modeList.push('concat');\n rawMessage = 'E0000M' + JSON.stringify(modeList);\n\n debugging('socketio') && console.debug(`${this.debugLabel} sending mode list to ${this.debugInfo.remoteLabel}`)\n if (this.socket)\n this.socket.send(rawMessage);\n else\n this.txPending.unshift({ message: rawMessage, msgSequence: ++this.msgSequence});\n }\n \n /* Internal method to exchange desired fragment size/max inflight messages/max fragments per message */\n sendMaxMessageInfo()\n {\n if (this.sentMessageInfo)\n return;\n\n /* Calculate the optimal maxFragmentSize based on maxHttpBufferSize.\n * Currently unsure of exactly what happens when socketio serializes our serialized\n * JSON. I suspect we may double-encode UTF-8 > 0x80. The largest UTF-8 character is\n * 5 bytes...so we ensure the maxFragmentSize is (maxHttpBufferSize / 5) - (fragmentOverhead * 5)\n * to allow for worst-case scenario\n */\n const fragmentOverhead = 1000 + 64 * 1024; /* derived from concatenation algorithm */\n\n let desiredMaxFragmentSize;\n if (this.options.maxHttpBufferSize)\n desiredMaxFragmentSize = (this.options.maxHttpBufferSize / 5) - (fragmentOverhead * 5);\n else\n desiredMaxFragmentSize = this.options.maxFragmentSize;\n const options = {\n maxFragmentSize: desiredMaxFragmentSize,\n maxFragments: this.options.maxFragmentCount,\n txMaxInFlight: this.txMaxInFlight,\n };\n\n const message = 'E0000B' + JSON.stringify(options);\n\n debugging('socketio') && console.debug(`${this.debugLabel} sending max http buffer size to ${this.debugInfo.remoteLabel}`);\n if (this.socket)\n this.socket.send(message);\n else\n this.txPending.push({ message: message, msgSequence: ++this.msgSequence });\n \n this.sentMessageInfo = true;\n }\n\n /**\n * Emit message events so that the connection can process them. The pending property is an\n * array which is used to order messages, so that they are emitted in the order they were\n * sent. There are edge cases in the concatenation code where a sender could theoretically\n * interleave multiple messages, and have a second, shorter, message fully received before \n * the first is fully transmitted.\n *\n * The pending property stores only strings or objects; objects are effectively positional\n * memoes which are turned into strings when they are ready to be emitted to the connection\n * for processing.\n */\n emitReadyMessages()\n {\n while (this.rxReady && this.rxPending.length && typeof this.rxPending[0] === 'string' && this.listenerCount('message') > 0)\n this.emit('message', this.rxPending.shift());\n }\n \n /** \n * Close the current instance of Socket.\n *\n * This function effectively finalizes the socket so that it can't be used any more and\n * (hopefully) does not entrain garbage. A 'close' event is emitted at the end of socket\n * finalization and should be hooked by things that might entrain this transport instance to free\n * up memory, like maybe a list of active transports, etc.\n *\n * We add few extra hops on/off the event loop here to try and clean up any messages halfway in/out\n * the door that we might want to process. That happens between the setting of the isClosed and \n * isFinal flags.\n *\n * @param {string} reason\n */\n #close(reason)\n {\n const that = this;\n debugging('socketio') && console.debug(this.debugLabel, 'closing connection' + (reason ? ` (${reason})` : ''));\n\n function finalize(socket)\n {\n if (that.isFinal)\n return;\n\n if (socket)\n socket.removeAllListeners();\n that.isFinal = true;\n\n /* Free up memory that might be huge in case something entrains us */\n that.rxPending.length = 0;\n that.txPending.length = 0;\n for (let msgId in that.rxFragments)\n that.rxFragments[msgId].length = 0;\n }\n\n function closeSocket()\n {\n const socket = that.socket;\n \n try\n {\n if (socket)\n socket.disconnect();\n that.isClosed = true;\n that.emit('close', reason);\n delete that.socket;\n }\n finally\n {\n finalize(socket);\n }\n }\n\n if (!this.isClosed)\n {\n /* If we called close, drain any pending messages and then disconnect */\n if (reason === 'api-call')\n {\n this.drainTxPending();\n setImmediate(()=>closeSocket());\n }\n else\n closeSocket();\n }\n }\n\n /* API - close the transport, free up memory. */\n close()\n {\n assert(arguments.length === 0)\n this.#close('api-call');\n }\n\n /**\n * Handle the socketio connect_error event, which is fired when:\n * - the low-level connection cannot be established\n * - the connection is denied by the server in a middleware function\n *\n * In the first case, socket.io will automatically try to reconnect, after a delay,\n * except that will cancel that reconnection attempt here by killing the transport\n * instance so that the Connection class can try failing over to an alternate transport.\n *\n * @param {Error} error optional instance of error describing the underlying reason for the\n * connect failure. If the underlying reason is expressible by an\n * HTTP status code, this code will be placed as a Number in the\n * description property.\n */\n handleConnectErrorEvent(error)\n {\n debugging('socketio') && console.debug(this.debugLabel, `unable to connect to ${this.url}`, error);\n if (error.type === 'TransportError' && typeof error.description === 'number')\n error.httpStatus = Number(error.description);\n\n this.emit('connect-failed', error);\n this.#close(error.message);\n }\n\n /** \n * Handle the socketio connect event, which is fired by the Socket instance upon initiator\n * connection and reconnection to a target.\n */\n handleConnectEvent()\n {\n if (this.hasConnected === true)\n {\n /* this transport is not supposed to reuse connections, and we call .close() on disconnect \n * to prevent this. However, the API does not guarantee that will be race-free.\n */\n debugging('socketio') && console.debug(this.debugLabel, `*** reconnected to ${this.debugInfo.remoteLabel} (ignoring)`);\n return;\n }\n\n /* initial connection */\n this.hasConnected = true;\n debugging('socketio') && console.debug(this.debugLabel, 'connected to', this.debugInfo.remoteLabel);\n\n this.initTimeout = setTimeout(() => this.#close('no mode message received'), this.options.establishTimeout) /* give target 10 second to reply with capabilities before assuming bad connection and crashing */\n if (dcpEnv.platform === 'nodejs')\n this.initTimeout.unref();\n \n this.drainTxPending();\n }\n\n /**\n * API: Send a message to the transport instance on the other side of this connection.\n *\n * @param {string} message the message to send\n */\n send(message)\n {\n const msgSequence = ++this.msgSequence;\n\n debugging('socketio') && console.debug(this.debugLabel, `sending message ${msgSequence} to`, this.debugInfo.remoteLabel);\n debugging('socketio') && debugging('verbose') && !debugging('all') && console.debug(this.debugLabel, message);\n\n if (!this.socket)\n throw new DCPError('SocketIOTransport.send: Not connected', 'DCPC-1110');\n \n if ( false\n || !(message.length > this.options.txMaxFragmentSize)\n || !this.mode.concat)\n this.txPending.push({ message, msgSequence });\n else\n { /* This is a large message. Use message concatenation to send in fragments. A failure of any\n * fragment is a failure of the entire message and will be handled at the connection layer.\n */\n let total = Math.ceil(message.length / this.options.txMaxFragmentSize);\n \n if (total > this.options.txMaxFragmentCount)\n throw new DCPError('extended message has more fragments than the peer will allow', 'DCP-1112');\n \n for (let seq=0; seq < total; seq++)\n {\n let start = seq * this.options.txMaxFragmentSize;\n let fragment = message.slice(start, start + this.options.txMaxFragmentSize);\n let header = JSON.stringify({ msgId: msgSequence, seq, total });\n let extMessage = 'E' + header.length.toString(16).padStart(4, '0') + 'C' + header + fragment;\n\n this.txPending.push({ message: extMessage, msgSequence, seq, total });\n\n /* This should be impossible, but it also effectively kills the connection. */\n if (header.length > 0xFFFF)\n throw new DCPError('extended message header too long', 'DCPC-1104');\n }\n }\n\n this.drainTxPending_soon();\n }\n\n drainTxPending_soon()\n {\n if (this.drainingSoon)\n return;\n this.drainingSoon = true;\n\n setImmediate(() => this.drainTxPending());\n }\n \n /**\n * Drain messages from the txPending queue out to socketio's network buffer. We throw away traffic\n * when it can't be sent; this is no different from packets already on the wire when the other end\n * has changed IP number, etc.\n *\n * In ack mode, this drain is controlled by acknowledgement messages so that socketio can have the\n * opportunity to insert ping/pong messages in the data stream while sending a message large enough\n * to need to be sent via concatenation. [ NOTE - it appears to send ping/pong OOB in version 4.4, \n * so currently ack mode is off by default / wg Mar 2022 ]\n *\n * The reason we put non-concat messages through here is to avoid special casing a bunch of nasty\n * things for the sake of a little tiny bit of performance. Sending message<-ACK and queueing through\n * the drain code is necessary if we wind up interleaving multiple message sends in some interesting\n * edge cases; notably, fast-teardown, but a future Connection class that ran without nonces (or multiple\n * nonces) would need this, too.\n *\n * In non-ack mode, the drain is mostly uncontrolled, but we throw each message on a separate pass of\n * the event loop, to try and keep huge message blasts one connection from overwhelming a busy server,\n * as well allowing socketio the chance to send its heartbeat messages and make it less likely that it\n * will hit its http buffer size limit, because that causes an immediate disconnect.\n *\n * Non-ack-mode is compatible with previous dcp5 versions and probably a little faster.\n */\n drainTxPending()\n {\n const that = this;\n\n if (this.txPending.length)\n debugging('socketio') && console.debug(this.debugLabel, `drain tx pending queue, ${this.txPending.length} ready`\n + (this.mode.ack ? `; ${this.txMaxInFlight - this.txNumInFlight} slots available` : ''));\n delete this.drainingSoon;\n\n if (this.isClosed)\n return;\n \n for (let i=0;\n !this.isClosed && this.txPending.length && (this.txNumInFlight < this.txMaxInFlight || !this.mode.ack);\n i++)\n {\n const pendingElement = this.txPending.shift();\n\n if (i === 0)\n writeToSocket(pendingElement);\n else\n setImmediateN(() => writeToSocket(pendingElement), i);\n\n if (this.mode.ack)\n this.txNumInFlight++;\n\n if (this.txPending.length === 0) /* just sent the last message */\n this.emit('drain');\n }\n\n if (this.txPending.length)\n this.drainTxPending_soon();\n \n function writeToSocket(pendingElement)\n {\n if (that.isClosed)\n return;\n\n const { message, msgSequence, seq, total } = pendingElement;\n that.socket.send(message);\n\n if (typeof seq !== 'undefined')\n debugging('socketio') && console.debug(that.debugLabel, `sent fragment ${seq + 1}/${total} of message ${msgSequence} (${message.length} bytes)`);\n else\n debugging('socketio') && console.debug(that.debugLabel, `sent message ${msgSequence} (${message.length} bytes)`);\n }\n }\n}\nSocketIOTransport.prototype.buildOptions = buildOptions;\n\n/** \n * Build the socketio options object, based on the passed-in options, dcp debug state, and internal\n * object state. Method shared between SocketIOTransport and SocketIOListener.\n */\nfunction buildOptions(options)\n{\n options = leafMerge(\n /* Baked-in Defaults */\n ({\n autoUnref: dcpEnv.platform === 'nodejs' ? true : undefined, /* socket.io + webpack 5 { node:global } work-around */\n perMessageDeflate: dcpConfig.build !== 'debug',\n maxFragmentCount: 1000, /* used to limit memory on receiver */\n maxFragmentSize: 1e6, /* bytes */\n txMaxFragmentCount:1000, /* peer's limit for fragments */\n txMaxFragmentSize: 1e6, /* peer's limit for fragment size */\n maxHttpBufferSize: false, /* bytes; false = auto */\n establishTimeout: 10 * 1000, /* 10 seconds */\n pingTimeout: 30 * 1e3, /* s */\n pingInterval: 90 * 1e3, /* s */\n transports: ['polling', 'websocket'],\n upgrade: true,\n rememberUpgrade: true,\n autoConnect: true,\n cors: {\n origin: '*',\n methods: ['GET', 'POST']\n },\n }),\n options,\n /* Absolutely mandatory */\n ({\n path: this.url.pathname // eslint-disable-line no-invalid-this\n }));\n\n /* draw out errors quickly in dev */\n if ((process.env.DCP_NETWORK_CONFIG_BUILD || dcpConfig.build) === 'debug')\n {\n /* short timeouts and debuggers don't get along well */\n if (dcpEnv.platform === 'nodejs' && !(requireNative('module')._cache.niim instanceof requireNative('module').Module))\n {\n options.pingTimeout /= 5;\n options.pingInterval /= 5;\n }\n }\n\n return options;\n}\n \n/**\n * @constructor\n * API - Factory which creates a new socketio listener, or throws. Emits 'listening' if the server \n * parameter is not provided and we create it, once it is ready for connections.\n * \n * @bug It is possible for a connection event to sneak in before the 'listening' event is fired, due to \n * the two-step event trigger. The right fix is to proxy the event, but dcp-events::event-emitter.proxy\n * itself currently has the same bug. For now, we just have callbacks in API. /wg apr 2022\n * \n * @param {object} config dcpConfig fragment - has .listen and .location\n * @param {object} target the instance of Target to bind to\n * @param {object} options [optional] A DCP config options variable (like dcpConfig.dcp.listenOptions)\n */\nfunction SocketIOListener(config, target, options)\n{\n var matchRegex;\n \n this.url = config.location;\n this.options = this.buildOptions(options);\n this.connSequence = 0;\n this.seq = ++listenerSeq;\n this.debugLabel = `socketio(l:${this.seq}):`;\n this.isShutdown = false;\n\n if (dcpEnv.platform !== 'nodejs')\n throw new Error('target mode only supported in nodejs');\n\n if (target.httpServer.listening)\n setImmediate(() => this.emit('listening', target.httpServer)); /* setImmediate to allow event listeners to be added before emitting */\n else\n target.httpServer.on('listening', () => this.emit('listening', target.httpServer));\n\n matchRegex = '^' + this.url.pathname;\n if (matchRegex.slice(-1) !== '/')\n matchRegex += '\\\\/';\n else\n matchRegex = matchRegex.slice(0, -1) + \"\\\\/\"\n\n matchRegex += '\\\\?EIO=[0-9]';\n debugging('socketio') && console.debug(this.debugLabel, `expecting requests to match regex ${matchRegex}`);\n\n target.all(new RegExp(matchRegex), (request) => {\n /* Socket.io does this intrinsically because it plays with the server's events directly,\n * but we \"handle\" it explicitly here so that we don't trip the 404 handler. \n */\n debugging('socketio') && console.debug(this.debugLabel, 'Handling', request.url);\n }, true);\n\n /* While we normally want to inherit options from parent to child socket via this.options,\n * we don't want this to happen for maxHttpBufferSize due to the way it is used and calculated\n * in the transport constructor. A buffer that is too small (eg false) will cause us to not\n * recognized socket.io SIDs.\n */\n \n this.options.maxHttpBufferSize = 1e7;\n \n const socketOptions = this.options;\n\n this.socketServer = requireNative('socket.io')(target.httpServer, socketOptions);\n this.socketServer.on('connection', (socket) => !this.isShutdown && this.handleConnectionEvent(config, socket));\n debugging() && console.debug(this.debugLabel, 'Socketio listener initialized for path', this.options.path);\n}\nSocketIOListener.prototype = new EventEmitter('socketio(l)');\nSocketIOListener.prototype.buildOptions = buildOptions;\n\nSocketIOListener.prototype.close = function SocketIOListener$$close()\n{\n if (!this.isShutdown)\n this.shutdown();\n this.socketServer.close(() => this.emit('close'));\n}\n\n/** Stop accepting new connections */\nSocketIOListener.prototype.shutdown = function SocketIOListener$$shutdown()\n{\n this.isShutdown = true;\n}\n\n/** Remove any event loop references used by this transport instance */\nSocketIOListener.prototype.unref = function SocketIOListener$$unref()\n{\n void false;\n}\n\n/**\n * Handle the socketio connection event, which is fired upon a connection from client.\n * Used by Target->SocketIOListener to create transport instances.\n *\n * It is expected that code hooking the 'connection' event will hook the 'message' in\n * the same pass of the event loop as it is invoked; otherwise, messages may be missed.\n *\n * @param {object} socket a new connection emitted by the socket.io server\n */\nSocketIOListener.prototype.handleConnectionEvent = function SocketIOListener$$handleConnectionEvent(config, socket)\n{\n var transport = new SocketIOTransport(config, this.options, socket);\n\n this.url = config.location || config.listen;\n this.connSequence++;\n transport.debugLabel = `socketio(t:${this.seq}.${this.connSequence}):`\n\n /* Ensure we don't emit any message events until the application or Connection class\n * has had the chance to set up message handlers via the connection event.\n */\n transport.rxReady = false;\n this.on('connection', () => setImmediate(() => {\n transport.rxReady = true;\n transport.emitReadyMessages();\n }));\n\n this.emit('connection', transport);\n}\n\n/* Define API */\nexports.TransportClass = SocketIOTransport;\nexports.Listener = SocketIOListener;\n\n\n//# sourceURL=webpack://dcp/./src/protocol-v4/transport/socketio.js?");
4848
+ eval("/* provided dependency */ var process = __webpack_require__(/*! ./node_modules/process/browser.js */ \"./node_modules/process/browser.js\");\n/**\n * @file transport/socketio.js\n * @author Ryan Rossiter, ryan@kingsds.network\n * @author Wes Garland, wes@kingsds.network \n * @date January 2020, March 2022\n *\n * This module implements the SocketIO Transport that is\n * used by the protocol to connect and communicate with peers using\n * SocketIO connections.\n *\n * Transport can operate in either ack mode or non-ack-mode, depending on the value of this.ackMode.\n * Ack mode sends an extra socketio-layer packet back after each message is received, and theoretically\n * this can be use tod meter or measure bandwidth or potentially second-guess socketio's ability to\n * interleave its heartbeat messages OOB from the main traffic. This mode could potentially be enabled\n * on-demand as well, although it appears that it is not necessary to get the desired behaviour at this\n * point, so the feature is off-by-default (although reasonably well-tested).\n */\n\n\nconst { Transport } = __webpack_require__(/*! . */ \"./src/protocol-v4/transport/index.js\");\nconst { EventEmitter } = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\");\nconst { leafMerge } = __webpack_require__(/*! dcp/utils/obj-merge */ \"./src/utils/obj-merge.js\");\nconst { DcpURL } = __webpack_require__(/*! dcp/common/dcp-url */ \"./src/common/dcp-url.js\");\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\nconst { setImmediateN } = __webpack_require__(/*! dcp/common/dcp-timers */ \"./src/common/dcp-timers.js\");\nconst { setImmediate } = __webpack_require__(/*! dcp/common/dcp-timers */ \"./src/common/dcp-timers.js\");\nconst { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\nconst { assert } = __webpack_require__(/*! dcp/common/dcp-assert */ \"./src/common/dcp-assert.js\");\n\n// Note: trying to `require('process')` in the browser prevents the portal\n// from loading. ~ER 20220803\n// const process = requireNative ? requireNative('process') : require('process');\nconst debugging = (__webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope)('dcp');\nconst dcpEnv = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\nconst bearer = __webpack_require__(/*! ./http-bearer */ \"./src/protocol-v4/transport/http-bearer.js\");\nconst semver = __webpack_require__(/*! semver */ \"./node_modules/semver/semver.js\");\n\nvar listenerSeq = 0;\nvar initiatorSeq = 0;\n\nif (debugging() && !process.env.DEBUG && dcpConfig.build === 'debug')\n{\n\n if (process.env.DEBUG)\n process.env.DEBUG += ',engine';\n else\n process.env.DEBUG = 'engine';\n\n if (debugging('socketio'))\n {\n /* DCP_DEBUG=dcp:socketio,dcp:verbose to see everything */\n process.env.DEBUG += ',socket.io-client:socket,socket.io:client,socket.io:server';\n if (debugging('verbose') && !debugging('all'))\n process.env.DEBUG += ',socket.io*,engine.io*'\n }\n}\n\nclass SocketIOTransport extends Transport\n{\n /**\n * @constructor\n * @param {socket} newSocket Target: an instance of socket.io Socket that represents\n * a new connection.\n * Initiator: unused\n */\n /**\n * @constructor create new instance of a socketio-flavoured Transport.\n *\n * @param {object} config dcpConfig fragment\n * @param {Url} url Initiator: has .location and optionally .friendLocation\n * Target: has .listen and .location\n *\n * @param {object} options The `socketio` property of a protocol-v4 connectionOptions \n * object, which was built from a combination of defaults and \n * various dcpConfig components.\n *\n * @returns instance suitable for target or initiator, depending on mode, but does not manage connections.\n */\n constructor(config, options={}, newSocket)\n {\n super('Protocol SocketIO Transport');\n const that = this;\n \n assert(DcpURL.isURL(config.location));\n assert(typeof options === 'object');\n \n this.name = 'socketio';\n this.url = config.location;\n this.msgSequence = 0;\n this.isClosed = false; /* true => connection finalization begun */\n this.isFinal = false; /* true => connection finalization completed */\n this.hasConnected = false; /* true => one 'connect' event has been processed */\n this.initTimeout = false; /* timeout when connecting to allow target to reply with their mode message + buffer size, crash if they don't reply */\n this.modeSet = false; /* true => received response mode message */\n this.bufferSizeSet = false; /* true => received target's buffer size */\n this.rxFragments = {}; /* messages we are receiving in fragments */\n this.rxPending = []; /* strings which are messages that are complete and ready to emit, or objects that in this.rxFragments */\n this.txPending = []; /* messages we are receiving in fragments */\n this.txNumInFlight = 0; /* number of txPending sent but not acknowleged */\n this.txMaxInFlight = 2; /* less than 2 causes bad perf when mode.ack */\n this.debugInfo = { remoteLabel: '<unknown>' };\n this.options = this.buildOptions(options);\n this.mode = {\n ack: this.options.enableModeAck ? true : false, /* Only turn on ack mode if specified */\n concat: this.options.enableModeConcat === false ? false : true, /* Only turn off concat mode if specified*/\n };\n\n\n if (!newSocket) /* Initiator */\n {\n this.debugLabel = `socketio(i:${++initiatorSeq}):`;\n this.rxReady = true; /* relax, said the night man */\n this.debugInfo.remoteLabel = config.location.href;\n if (this.options.disableModeAnnouncement !== false)\n this.sendModeList();\n this.sendMaxMessageInfo();\n\n bearer.a$isFriendlyUrl(config.friendLocation).then((useFriendLocation) => {\n const connectUrl = useFriendLocation ? config.friendLocation : config.location;\n\n debugging('socketio') && console.log(this.debugLabel, 'connecting to', connectUrl.href);\n this.socket = (__webpack_require__(/*! socket.io-client */ \"./node_modules/socket.io-client/build/cjs/index.js\").connect)(connectUrl.origin, this.options);\n this.socket.on('connect_error', (error) => !this.isClosed && this.handleConnectErrorEvent(error));\n this.socket.on('connect', () => !this.isClosed && this.handleConnectEvent());\n\n useSocket();\n }).catch((error) => {\n debugging() && console.error(this.debugLabel, error);\n this.emit('error', error);\n });\n }\n else /* Target - receives socketio instance from SocketIOListener.handleConnectionEvent */\n {\n if (dcpEnv.platform !== 'nodejs')\n throw new Error('socketio: target mode only supported in nodejs');\n\n this.debugLabel = 'socketio(t:<new>):';\n\n this.socket = newSocket;\n this.debugInfo.remoteLabel = this.socket.handshake.address.replace(/^::ffff:/,'');\n debugging('socketio') && console.debug(this.debugLabel, `new socket from ${this.debugInfo.remoteLabel}`\n + ` on ${this.socket.handshake && this.socket.handshake.url}, ${this.socket.id}`);\n\n useSocket();\n }\n\n function useSocket()\n {\n that.socket.compress(dcpConfig.build !== 'debug'); /* try to keep traffic sniffable in debug */\n that.socket.on('message', (msg) => !that.isFinal && that.handleMessageEvent(msg));\n that.socket.on('disconnect', (reason) => !that.isClosed && that.handleDisconnectEvent(reason));\n }\n }\n\n /**\n * API - determine if a transport is usable or not.\n * @returns true if the transport is not closed.\n */\n ready()\n {\n return !this.isClosed;\n }\n \n /**\n * Handle the socket.io disconnect event, which is fired upon disconnection.\n * For some reasons (explicit disconnection), the socket.io code will not try to reconnect, but\n * in all other cases, the socket.io client will normally wait for a small random delay and then \n * try to reconnect, but in our case, we simply tear the transport down and let the Connection\n * class handle reconnects, since it might prefer a different transport at this point anyhow.\n *\n * One tricky case here is ping timeout, since the ping timeout includes the time to transmit the\n * packet before it was sent.\n *\n * @param {string} reason Possible reasons for disconnection.\n */\n handleDisconnectEvent(reason)\n {\n debugging('socketio') && console.debug(this.debugLabel, `disconnected from ${this.debugInfo.remoteLabel}; reason=${reason}`);\n\n if (this.isClosed !== true)\n setImmediate(() => this.#close(reason));\n\n switch(reason)\n {\n case 'io client disconnect':\n return; /* we called socket.disconnect() from another \"thread\" */\n case 'io server disconnect':\n case 'ping timeout':\n case 'transport close':\n case 'transport error':\n this.emit('end', reason);\n /* fallthrough */\n default:\n }\n }\n\n /**\n * Handle a socketio message from the peer.\n *\n * Most of the time, a socketio message contains a DCP message, however we support other message\n * types as well, with a mechanism inspired by 3GPP TS 23.040 (GSM 03.40) message concatenation.\n */\n handleMessageEvent(msg)\n {\n if (typeof msg !== 'string')\n {\n debugging('socketio') && console.debug(this.debugLabel, `received ${typeof msg} message from peer`, this.debugInfo.remoteLabel);\n return;\n }\n\n try\n {\n switch (msg[0])\n {\n case '{': /* all DCP Messages are JSON */\n this.processMessage(msg);\n break;\n case 'E':\n this.processExtendedMessage(msg);\n break;\n case 'A':\n this.processAcknowledgement(msg);\n break;\n case 'T':\n debugging('socketio') && console.debug(this.debugLabel, 'Received debug message:', msg);\n break;\n default:\n /* adhoc messages not supported in this transport */\n throw new DCPError(this.debugLabel, `Unrecognized message type indicator from ${this.debugInfo.remoteLabel} (${msg.charCodeAt(0)})`, 'DCPC-1102');\n }\n }\n catch(error)\n {\n debugging('socketio') && console.debug(this.debugLabel, 'handleMessageEvent: transport error, closing\\n ', error);\n this.emit('error', error);\n setImmediate(() => this.#close(error.message));\n }\n }\n\n /**\n * Process an ordinary message. This is just a hunk of JSON that gets passed up to the connection.\n * @param {string} msg\n */\n processMessage(msg)\n {\n this.rxPending.push(msg);\n this.emitReadyMessages();\n if (this.mode.ack)\n this.socket.send('A0000-ACK'); /* Ack w/o header = normal message */\n }\n\n /**\n * Remote has acknowledged the receipt of a message fragment. When in ack mode, this\n * triggers us to send more messages from the txPending queue. We always ack event when \n * not in ack-mode. See processExtendedMessage() comment for header description.\n *\n * Future: this is where maxInFlight might be tuned, based on how fast we get here\n *\n * @param {string} _msg\n */\n processAcknowledgement(_msg)\n {\n if (this.txNumInFlight > 0)\n this.txNumInFlight--;\n else\n {\n if (this.options.strict || !this.mode.ack)\n throw new DCPError(this.debugLabel, `Synchronization error: received unexpected acknowledgement message from ${this.debugInfo.remoteLabel}`, 'DCPC-1109');\n else\n this.mode.ack = true;\n }\n \n this.drainTxPending_soon();\n }\n\n /**\n * Extended messages have the following format:\n * Octet(s) Description\n * 0 E (69) or A (65)\n * 1..4 Header size (N) in hexadecimal\n * 5 Header Type\n * C (67) => concatenation\n * 6..6 + N: Header\n *\n * Upon receipt, extended messages are acknowledged by retransmitting their header, but\n * with the first character changed from E to A.\n *\n * @param {string} msg\n */\n processExtendedMessage(msg)\n {\n const headerSize = parseInt(msg.slice(1,5), 16);\n const headerType = msg[5];\n const header = msg.slice(6, 6 + headerSize);\n\n switch(headerType)\n {\n case 'M':\n this.processExtendedMessage_mode(header, msg.slice(6 + headerSize));\n break;\n case 'B':\n this.processExtendedMessage_maxBufferSize(msg.slice(6 + headerSize));\n break;\n case 'C':\n this.processExtendedMessage_concatenated(header, msg.slice(6 + headerSize));\n break;\n default:\n if (this.options.strict)\n throw new DCPError(`Unrecognized extended message header type indicator (${msg.charCodeAt(5)})`, 'DCPC-1101');\n }\n\n if (this.mode.ack)\n this.socket.send('A' + msg.slice(1, 6 + headerSize));\n }\n\n /**\n * We have received a fragment of a concatenation extended message. Memoize the fragment, and\n * if this is the last fragment, transform the fragments list into a pending message string and\n * then emits all ready messages (pending message strings).\n *\n * This code is capable of handling messages whose fragments arrive out of order. Complete messages \n * are emitted in the order that their first parts are received. Socketio is supposed to be total\n * order-aware but I wanted a safety belt for intermingled messages without occupying the entire\n * socketio network buffer on the first pass of the event loop. Because the first message fragment\n * is emitted to socketio on the same event loop pass as their Connection::send calls, I believe\n * that are no cases where messages will be emitted out of order at the other end, even when \n * intermingled on the wire, and the Nth message is much longer than any (N+k)th messages.\n *\n * @param {object} header the extended message header\n * @param {string} payload the payload of the extended message, i.e. a message fragment\n */\n processExtendedMessage_concatenated(header, payload)\n {\n try\n {\n header = JSON.parse(header);\n }\n catch(e)\n {\n throw new DCPError(`invalid extended message header '${header}'`, 'DCPC-1103');\n }\n\n if (!this.options.strict && !this.mode.concat)\n this.mode.concat = true;\n \n if (!(header.total <= this.options.maxFragmentCount))\n throw new DCPError('excessive message fragmentation', 'DCPC-1107'); /* make it more difficult for attacker to force OOM us */\n\n if (!(header.seq < header.total))\n throw new DCPError(`corrupt header for part ${header.seq + 1} of ${header.total} of message ${header.msgId}`, 'DCPC-1108');\n \n /* First fragment received, initial data structures and memoize the message's arrival order via pending list */\n if (!this.rxFragments[header.msgId])\n {\n const fragments = [];\n this.rxFragments[header.msgId] = fragments;\n this.rxPending.push(fragments);\n }\n\n this.rxFragments[header.msgId][header.seq] = payload;\n debugging('socketio') && console.debug(this.debugLabel, 'received part', header.seq + 1, 'of', header.total, 'for message', header.msgId);\n\n if (header.total !== this.rxFragments[header.msgId].filter(el => el !== undefined).length)\n return;\n\n debugging('socketio') && console.debug(this.debugLabel, 'received all parts of message', header.msgId);\n \n const idx = this.rxPending.indexOf(this.rxFragments[header.msgId])\n this.rxPending[idx] = this.rxFragments[header.msgId].join('');\n delete this.rxFragments[header.msgId];\n this.emitReadyMessages();\n }\n\n processExtendedMessage_mode(_header, payload)\n {\n var modeList;\n \n try\n {\n modeList = JSON.parse(payload);\n }\n catch(e)\n {\n throw new DCPError(`invalid mode message payload '${payload}'`, 'DCPC-1105');\n }\n\n for (let mode of modeList)\n {\n debugging('socketio') && console.debug(this.debugLabel, ` - remote supports ${mode} mode`); \n switch(mode)\n {\n case 'ack':\n this.mode.ack = true;\n break;\n case 'concat':\n this.mode.concat = true;\n break;\n default:\n debugging('socketio') && console.debug(this.debugLabel, ' - remote requested unrecognized mode:', mode);\n break;\n }\n }\n\n // true if we are connecting a new transport, expecting the initial mode reply, and have received the target's buffer size\n if (this.initTimeout && this.bufferSizeSet)\n {\n clearTimeout(this.initTimeout);\n delete this.initTimeout;\n this.emit('connect');\n }\n this.modeSet = true;\n this.sendModeList();\n }\n \n processExtendedMessage_maxBufferSize(payload)\n {\n var receivedOptions = JSON.parse(payload); \n \n this.options.txMaxFragmentSize = Math.floor(receivedOptions.maxFragmentSize / 11); /* XXX EMERGENCY BUGFIX - Aug 2022 /wg */\n this.options.txMaxFragmentCount = receivedOptions.maxFragmentCount;\n\n debugging() && console.debug(this.debugLabel, 'Set max transmit fragment size to', this.options.txMaxFragmentSize);\n\n if (receivedOptions.txMaxInFlight < this.txMaxInFlight)\n this.txMaxInFlight = receivedOptions.txMaxInFlight;\n if (this.options.maxFragmentSize < 0)\n throw new DCPError(`maxFragmentSize was set to < 1, (${this.options.txMaxFragmentSize}). Server requested bad fragment size, cannot connect.`, 'DCPC-1111');\n if (this.txMaxInFlight < 1)\n throw new DCPError(`txMaxInFlight was set to < 1, (${this.txMaxInFlight}). Cannot send messages with fewer than 1 in flight.`, 'DCPC-1111');\n \n // true if we are connecting a new transport, expecting the initial buffer reply, and have received the target's mode designation\n if (this.initTimeout && this.modeSet)\n {\n clearTimeout(this.initTimeout);\n delete this.initTimeout;\n this.emit('connect');\n }\n this.bufferSizeSet = true;\n\n this.sendMaxMessageInfo(); \n }\n\n /* Internal method to tell peer what we can receive */\n sendModeList()\n {\n var rawMessage;\n var modeList = [];\n\n if (this.sentModeList)\n return;\n\n this.sentModeList = true;\n this.mode.ack && modeList.push('ack');\n this.mode.concat && modeList.push('concat');\n rawMessage = 'E0000M' + JSON.stringify(modeList);\n\n debugging('socketio') && console.debug(`${this.debugLabel} sending mode list to ${this.debugInfo.remoteLabel}`)\n if (this.socket)\n this.socket.send(rawMessage);\n else\n this.txPending.unshift({ message: rawMessage, msgSequence: ++this.msgSequence});\n }\n \n /* Internal method to exchange desired fragment size/max inflight messages/max fragments per message */\n sendMaxMessageInfo()\n {\n if (this.sentMessageInfo)\n return;\n\n /* Calculate the optimal maxFragmentSize based on maxHttpBufferSize.\n * Currently unsure of exactly what happens when socketio serializes our serialized\n * JSON. I suspect we may double-encode UTF-8 > 0x80. The largest UTF-8 character is\n * 5 bytes...so we ensure the maxFragmentSize is (maxHttpBufferSize / 5) - (fragmentOverhead * 5)\n * to allow for worst-case scenario\n */\n const fragmentOverhead = 1000 + 64 * 1024; /* derived from concatenation algorithm */\n\n let desiredMaxFragmentSize;\n if (this.options.maxHttpBufferSize)\n desiredMaxFragmentSize = (this.options.maxHttpBufferSize / 5) - (fragmentOverhead * 5);\n else\n desiredMaxFragmentSize = this.options.maxFragmentSize;\n const options = {\n maxFragmentSize: desiredMaxFragmentSize,\n maxFragments: this.options.maxFragmentCount,\n txMaxInFlight: this.txMaxInFlight,\n };\n\n const message = 'E0000B' + JSON.stringify(options);\n\n debugging('socketio') && console.debug(`${this.debugLabel} sending max http buffer size to ${this.debugInfo.remoteLabel}`);\n if (this.socket)\n this.socket.send(message);\n else\n this.txPending.push({ message: message, msgSequence: ++this.msgSequence });\n \n this.sentMessageInfo = true;\n }\n\n /**\n * Emit message events so that the connection can process them. The pending property is an\n * array which is used to order messages, so that they are emitted in the order they were\n * sent. There are edge cases in the concatenation code where a sender could theoretically\n * interleave multiple messages, and have a second, shorter, message fully received before \n * the first is fully transmitted.\n *\n * The pending property stores only strings or objects; objects are effectively positional\n * memoes which are turned into strings when they are ready to be emitted to the connection\n * for processing.\n */\n emitReadyMessages()\n {\n while (this.rxReady && this.rxPending.length && typeof this.rxPending[0] === 'string' && this.listenerCount('message') > 0)\n this.emit('message', this.rxPending.shift());\n }\n \n /** \n * Close the current instance of Socket.\n *\n * This function effectively finalizes the socket so that it can't be used any more and\n * (hopefully) does not entrain garbage. A 'close' event is emitted at the end of socket\n * finalization and should be hooked by things that might entrain this transport instance to free\n * up memory, like maybe a list of active transports, etc.\n *\n * We add few extra hops on/off the event loop here to try and clean up any messages halfway in/out\n * the door that we might want to process. That happens between the setting of the isClosed and \n * isFinal flags.\n *\n * @param {string} reason\n */\n #close(reason)\n {\n const that = this;\n debugging('socketio') && console.debug(this.debugLabel, 'closing connection' + (reason ? ` (${reason})` : ''));\n\n function finalize(socket)\n {\n if (that.isFinal)\n return;\n\n if (socket)\n socket.removeAllListeners();\n that.isFinal = true;\n\n /* Free up memory that might be huge in case something entrains us */\n that.rxPending.length = 0;\n that.txPending.length = 0;\n for (let msgId in that.rxFragments)\n that.rxFragments[msgId].length = 0;\n }\n\n function closeSocket()\n {\n const socket = that.socket;\n \n try\n {\n if (socket)\n socket.disconnect();\n that.isClosed = true;\n that.emit('close', reason);\n delete that.socket;\n }\n finally\n {\n finalize(socket);\n }\n }\n\n if (!this.isClosed)\n {\n /* If we called close, drain any pending messages and then disconnect */\n if (reason === 'api-call')\n {\n this.drainTxPending();\n setImmediate(()=>closeSocket());\n }\n else\n closeSocket();\n }\n }\n\n /* API - close the transport, free up memory. */\n close()\n {\n assert(arguments.length === 0)\n this.#close('api-call');\n }\n\n /**\n * Handle the socketio connect_error event, which is fired when:\n * - the low-level connection cannot be established\n * - the connection is denied by the server in a middleware function\n *\n * In the first case, socket.io will automatically try to reconnect, after a delay,\n * except that will cancel that reconnection attempt here by killing the transport\n * instance so that the Connection class can try failing over to an alternate transport.\n *\n * @param {Error} error optional instance of error describing the underlying reason for the\n * connect failure. If the underlying reason is expressible by an\n * HTTP status code, this code will be placed as a Number in the\n * description property.\n */\n handleConnectErrorEvent(error)\n {\n debugging('socketio') && console.debug(this.debugLabel, `unable to connect to ${this.url}`, error);\n if (error.type === 'TransportError' && typeof error.description === 'number')\n error.httpStatus = Number(error.description);\n\n this.emit('connect-failed', error);\n this.#close(error.message);\n }\n\n /** \n * Handle the socketio connect event, which is fired by the Socket instance upon initiator\n * connection and reconnection to a target.\n */\n handleConnectEvent()\n {\n if (this.hasConnected === true)\n {\n /* this transport is not supposed to reuse connections, and we call .close() on disconnect \n * to prevent this. However, the API does not guarantee that will be race-free.\n */\n debugging('socketio') && console.debug(this.debugLabel, `*** reconnected to ${this.debugInfo.remoteLabel} (ignoring)`);\n return;\n }\n\n /* initial connection */\n this.hasConnected = true;\n debugging('socketio') && console.debug(this.debugLabel, 'connected to', this.debugInfo.remoteLabel);\n\n this.initTimeout = setTimeout(() => this.#close('no mode message received'), this.options.establishTimeout) /* give target 10 second to reply with capabilities before assuming bad connection and crashing */\n if (dcpEnv.platform === 'nodejs')\n this.initTimeout.unref();\n \n this.drainTxPending();\n }\n\n /**\n * API: Send a message to the transport instance on the other side of this connection.\n *\n * @param {string} message the message to send\n */\n send(message)\n {\n const msgSequence = ++this.msgSequence;\n\n debugging('socketio') && console.debug(this.debugLabel, `sending message ${msgSequence} to`, this.debugInfo.remoteLabel);\n debugging('socketio') && debugging('verbose') && !debugging('all') && console.debug(this.debugLabel, message);\n\n if (!this.socket)\n throw new DCPError('SocketIOTransport.send: Not connected', 'DCPC-1110');\n \n if ( false\n || !(message.length > this.options.txMaxFragmentSize)\n || !this.mode.concat)\n this.txPending.push({ message, msgSequence });\n else\n { /* This is a large message. Use message concatenation to send in fragments. A failure of any\n * fragment is a failure of the entire message and will be handled at the connection layer.\n */\n let total = Math.ceil(message.length / this.options.txMaxFragmentSize);\n \n if (total > this.options.txMaxFragmentCount)\n throw new DCPError('extended message has more fragments than the peer will allow', 'DCP-1112');\n \n for (let seq=0; seq < total; seq++)\n {\n let start = seq * this.options.txMaxFragmentSize;\n let fragment = message.slice(start, start + this.options.txMaxFragmentSize);\n let header = JSON.stringify({ msgId: msgSequence, seq, total });\n let extMessage = 'E' + header.length.toString(16).padStart(4, '0') + 'C' + header + fragment;\n\n this.txPending.push({ message: extMessage, msgSequence, seq, total });\n\n /* This should be impossible, but it also effectively kills the connection. */\n if (header.length > 0xFFFF)\n throw new DCPError('extended message header too long', 'DCPC-1104');\n }\n }\n\n this.drainTxPending_soon();\n }\n\n drainTxPending_soon()\n {\n if (this.drainingSoon)\n return;\n this.drainingSoon = true;\n\n setImmediate(() => this.drainTxPending());\n }\n \n /**\n * Drain messages from the txPending queue out to socketio's network buffer. We throw away traffic\n * when it can't be sent; this is no different from packets already on the wire when the other end\n * has changed IP number, etc.\n *\n * In ack mode, this drain is controlled by acknowledgement messages so that socketio can have the\n * opportunity to insert ping/pong messages in the data stream while sending a message large enough\n * to need to be sent via concatenation. [ NOTE - it appears to send ping/pong OOB in version 4.4, \n * so currently ack mode is off by default / wg Mar 2022 ]\n *\n * The reason we put non-concat messages through here is to avoid special casing a bunch of nasty\n * things for the sake of a little tiny bit of performance. Sending message<-ACK and queueing through\n * the drain code is necessary if we wind up interleaving multiple message sends in some interesting\n * edge cases; notably, fast-teardown, but a future Connection class that ran without nonces (or multiple\n * nonces) would need this, too.\n *\n * In non-ack mode, the drain is mostly uncontrolled, but we throw each message on a separate pass of\n * the event loop, to try and keep huge message blasts one connection from overwhelming a busy server,\n * as well allowing socketio the chance to send its heartbeat messages and make it less likely that it\n * will hit its http buffer size limit, because that causes an immediate disconnect.\n *\n * Non-ack-mode is compatible with previous dcp5 versions and probably a little faster.\n */\n drainTxPending()\n {\n const that = this;\n\n if (this.txPending.length)\n debugging('socketio') && console.debug(this.debugLabel, `drain tx pending queue, ${this.txPending.length} ready`\n + (this.mode.ack ? `; ${this.txMaxInFlight - this.txNumInFlight} slots available` : ''));\n delete this.drainingSoon;\n\n if (this.isClosed)\n return;\n \n for (let i=0;\n !this.isClosed && this.txPending.length && (this.txNumInFlight < this.txMaxInFlight || !this.mode.ack);\n i++)\n {\n const pendingElement = this.txPending.shift();\n\n if (i === 0)\n writeToSocket(pendingElement);\n else\n setImmediateN(() => writeToSocket(pendingElement), i);\n \n if (this.mode.ack)\n this.txNumInFlight++;\n\n if (this.txPending.length === 0) /* just sent the last message */\n this.emit('drain');\n }\n\n if (this.txPending.length)\n this.drainTxPending_soon();\n \n function writeToSocket(pendingElement)\n {\n if (that.isClosed)\n return;\n\n const { message, msgSequence, seq, total } = pendingElement;\n that.socket.send(message);\n\n if (typeof seq !== 'undefined')\n debugging('socketio') && console.debug(that.debugLabel, `sent fragment ${seq + 1}/${total} of message ${msgSequence} (${message.length} bytes)`);\n else\n debugging('socketio') && console.debug(that.debugLabel, `sent message ${msgSequence} (${message.length} bytes)`);\n }\n }\n}\nSocketIOTransport.prototype.buildOptions = buildOptions;\n\n/** \n * Build the socketio options object, based on the passed-in options, dcp debug state, and internal\n * object state. Method shared between SocketIOTransport and SocketIOListener.\n */\nfunction buildOptions(options)\n{\n options = leafMerge(\n /* Baked-in Defaults */\n ({\n autoUnref: dcpEnv.platform === 'nodejs' ? true : undefined, /* socket.io + webpack 5 { node:global } work-around */\n perMessageDeflate: dcpConfig.build !== 'debug',\n maxFragmentCount: 1000, /* used to limit memory on receiver */\n maxFragmentSize: 1e6, /* bytes */\n txMaxFragmentCount:1000, /* peer's limit for fragments */\n txMaxFragmentSize: 1e6, /* peer's limit for fragment size */\n maxHttpBufferSize: false, /* bytes; false = auto */\n establishTimeout: 10 * 1000, /* 10 seconds */\n pingTimeout: 30 * 1e3, /* s */\n pingInterval: 90 * 1e3, /* s */\n transports: ['polling', 'websocket'],\n upgrade: true,\n rememberUpgrade: true,\n autoConnect: true,\n cors: {\n origin: '*',\n methods: ['GET', 'POST']\n },\n }),\n options,\n /* Absolutely mandatory */\n ({\n path: this.url.pathname // eslint-disable-line no-invalid-this\n }));\n\n /* draw out errors quickly in dev */\n if ((process.env.DCP_NETWORK_CONFIG_BUILD || dcpConfig.build) === 'debug')\n {\n /* short timeouts and debuggers don't get along well */\n if (dcpEnv.platform === 'nodejs' && !(requireNative('module')._cache.niim instanceof requireNative('module').Module))\n {\n options.pingTimeout /= 5;\n options.pingInterval /= 5;\n }\n }\n\n return options;\n}\n \n/**\n * @constructor\n * API - Factory which creates a new socketio listener, or throws. Emits 'listening' if the server \n * parameter is not provided and we create it, once it is ready for connections.\n * \n * @bug It is possible for a connection event to sneak in before the 'listening' event is fired, due to \n * the two-step event trigger. The right fix is to proxy the event, but dcp-events::event-emitter.proxy\n * itself currently has the same bug. For now, we just have callbacks in API. /wg apr 2022\n * \n * @param {object} config dcpConfig fragment - has .listen and .location\n * @param {object} target the instance of Target to bind to\n * @param {object} options [optional] A DCP config options variable (like dcpConfig.dcp.listenOptions)\n */\nfunction SocketIOListener(config, target, options)\n{\n var matchRegex;\n \n this.url = config.location;\n this.options = this.buildOptions(options);\n this.connSequence = 0;\n this.seq = ++listenerSeq;\n this.debugLabel = `socketio(l:${this.seq}):`;\n this.isShutdown = false;\n\n if (dcpEnv.platform !== 'nodejs')\n throw new Error('target mode only supported in nodejs');\n\n if (target.httpServer.listening)\n setImmediate(() => this.emit('listening', target.httpServer)); /* setImmediate to allow event listeners to be added before emitting */\n else\n target.httpServer.on('listening', () => this.emit('listening', target.httpServer));\n\n matchRegex = '^' + this.url.pathname;\n if (matchRegex.slice(-1) !== '/')\n matchRegex += '\\\\/';\n else\n matchRegex = matchRegex.slice(0, -1) + \"\\\\/\"\n\n matchRegex += '\\\\?EIO=[0-9]';\n debugging('socketio') && console.debug(this.debugLabel, `expecting requests to match regex ${matchRegex}`);\n\n target.all(new RegExp(matchRegex), (request) => {\n /* Socket.io does this intrinsically because it plays with the server's events directly,\n * but we \"handle\" it explicitly here so that we don't trip the 404 handler. \n */\n debugging('socketio') && console.debug(this.debugLabel, 'Handling', request.url);\n }, true);\n\n /* While we normally want to inherit options from parent to child socket via this.options,\n * we don't want this to happen for maxHttpBufferSize due to the way it is used and calculated\n * in the transport constructor. A buffer that is too small (eg false) will cause us to not\n * recognized socket.io SIDs.\n */\n \n this.options.maxHttpBufferSize = this.options.maxHttpBufferSize || 1e9;\n \n const socketOptions = this.options;\n\n this.socketServer = requireNative('socket.io')(target.httpServer, socketOptions);\n this.socketServer.on('connection', (socket) => !this.isShutdown && this.handleConnectionEvent(config, socket));\n debugging() && console.debug(this.debugLabel, 'Socketio listener initialized for path', this.options.path);\n}\nSocketIOListener.prototype = new EventEmitter('socketio(l)');\nSocketIOListener.prototype.buildOptions = buildOptions;\n\nSocketIOListener.prototype.close = function SocketIOListener$$close()\n{\n if (!this.isShutdown)\n this.shutdown();\n this.socketServer.close(() => this.emit('close'));\n}\n\n/** Stop accepting new connections */\nSocketIOListener.prototype.shutdown = function SocketIOListener$$shutdown()\n{\n this.isShutdown = true;\n}\n\n/** Remove any event loop references used by this transport instance */\nSocketIOListener.prototype.unref = function SocketIOListener$$unref()\n{\n void false;\n}\n\n/**\n * Handle the socketio connection event, which is fired upon a connection from client.\n * Used by Target->SocketIOListener to create transport instances.\n *\n * It is expected that code hooking the 'connection' event will hook the 'message' in\n * the same pass of the event loop as it is invoked; otherwise, messages may be missed.\n *\n * @param {object} socket a new connection emitted by the socket.io server\n */\nSocketIOListener.prototype.handleConnectionEvent = function SocketIOListener$$handleConnectionEvent(config, socket)\n{\n var transport = new SocketIOTransport(config, this.options, socket);\n\n this.url = config.location || config.listen;\n this.connSequence++;\n transport.debugLabel = `socketio(t:${this.seq}.${this.connSequence}):`\n\n /* Ensure we don't emit any message events until the application or Connection class\n * has had the chance to set up message handlers via the connection event.\n */\n transport.rxReady = false;\n this.on('connection', () => setImmediate(() => {\n transport.rxReady = true;\n transport.emitReadyMessages();\n }));\n\n this.emit('connection', transport);\n}\n\n/* Define API */\nexports.TransportClass = SocketIOTransport;\nexports.Listener = SocketIOListener;\n\n\n//# sourceURL=webpack://dcp/./src/protocol-v4/transport/socketio.js?");
4719
4849
 
4720
4850
  /***/ }),
4721
4851
 
@@ -4740,13 +4870,24 @@ eval("const DCP_ENV = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/comm
4740
4870
 
4741
4871
  /***/ }),
4742
4872
 
4873
+ /***/ "./src/utils/content-encoding.js":
4874
+ /*!***************************************!*\
4875
+ !*** ./src/utils/content-encoding.js ***!
4876
+ \***************************************/
4877
+ /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
4878
+
4879
+ eval("/**\n * @file encodings.js\n * Utilities for working with various content transfer\n * encodings, like base64.\n * @author Wes Garland, wes@kingsds.network\n * @date Feb 2022\n */\n\n exports.atob = globalThis.atob || __webpack_require__(/*! atob */ \"./node_modules/atob/browser-atob.js\");\n exports.btoa = globalThis.btoa || __webpack_require__(/*! btoa */ \"./node_modules/btoa/index.js\");\n\n//# sourceURL=webpack://dcp/./src/utils/content-encoding.js?");
4880
+
4881
+ /***/ }),
4882
+
4743
4883
  /***/ "./src/utils/encodeDataURI.js":
4744
4884
  /*!************************************!*\
4745
4885
  !*** ./src/utils/encodeDataURI.js ***!
4746
4886
  \************************************/
4747
4887
  /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
4748
4888
 
4749
- eval("/** \n * @file encodeDataURI.js\n * @author Nazila Akhavan <nazila@kingsds.network>\n * @date Sep 2020\n * \n * Encode input and return the URI.\n */\nconst kvin = __webpack_require__(/*! kvin */ \"./node_modules/kvin/kvin.js\");\nconst DCP_ENV = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\n\nif (!DCP_ENV.isBrowserPlatform) {\n btoa = __webpack_require__(/*! btoa */ \"./node_modules/btoa/index.js\");\n}\n/**\n * Encode input to data:[<MIME-type>][;base64][;charset=<encoding>],<data>\n * @param { string | object } input \n * @param { string } [MIMEType] \n */\nexports.encodeDataURI = function utils$$encodeDataURI (input, MIMEType) {\n let inputType = typeof input;\n let temp, encoding;\n let textType, imageType;\n if(MIMEType) {\n temp = MIMEType.split(';');\n MIMEType = temp[0];\n if(temp[1]) encoding = temp[1];\n textType = MIMEType.match(/text\\/.*/);\n textType = textType? textType[0] : [];\n imageType = MIMEType.match(/image\\/.*/);\n imageType = imageType? imageType[0] : [];\n } else {\n switch (inputType) {\n case 'string':\n MIMEType = 'text/plain';\n break;\n\n case 'boolean':\n MIMEType = 'application/json';\n break;\n\n default:\n MIMEType = 'application/x-kvin';\n break;\n }\n }\n\n switch (MIMEType) {\n case 'application/javascript': \n case 'text/plain':\n if(encoding === 'base64') {\n return 'data:text/plain;base64,' + btoa(input);\n }\n return 'data:,' + encodeURI(input);\n\n case 'application/json':\n return 'data:application/json,' + encodeURI(JSON.stringify(input));\n\n case 'application/x-kvin':\n return 'data:application/x-kvin,' + encodeURI(kvin.serialize(input));\n\n case 'application/octet-stream':\n input = new Uint8Array(input);\n return 'data:application/octet-stream;base64,' + btoa(input); \n\n case textType:\n return `data:${textType},` + encodeURI(input);\n\n case imageType:\n if (inputType === 'string') return `data:${imageType};base64,` + btoa(input);\n input = new Uint8Array(input.buffer)\n return encodeURI(`data:${imageType};base64,` + btoa(input)); \n\n default:\n throw new Error(`The content type ${MIMEType} is not supported in encodeDataURI()!`)\n }\n}\n\n// https://stackoverflow.com/questions/11089732/display-image-from-blob-using-javascript-and-websockets\n// public method for encoding an Uint8Array to base64\n\nfunction encode(input) {\n var keyStr = \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=\";\n var output = \"\";\n var chr1, chr2, chr3, enc1, enc2, enc3, enc4;\n var i = 0;\n\n while (i < input.length) {\n chr1 = input[i++];\n chr2 = i < input.length ? input[i++] : Number.NaN; // Not sure if the index \n chr3 = i < input.length ? input[i++] : Number.NaN; // checks are needed here\n\n enc1 = chr1 >> 2;\n enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);\n enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);\n enc4 = chr3 & 63;\n\n if (isNaN(chr2)) {\n enc3 = enc4 = 64;\n } else if (isNaN(chr3)) {\n enc4 = 64;\n }\n output += keyStr.charAt(enc1) + keyStr.charAt(enc2) +\n keyStr.charAt(enc3) + keyStr.charAt(enc4);\n }\n return output;\n}\n\n \n \n\n\n//# sourceURL=webpack://dcp/./src/utils/encodeDataURI.js?");
4889
+ "use strict";
4890
+ eval("/** \n * @file encodeDataURI.js\n * @author Nazila Akhavan <nazila@kingsds.network>\n * @date Sep 2020\n * \n * Encode input and return the URI.\n */\n\n\nconst kvin = __webpack_require__(/*! kvin */ \"./node_modules/kvin/kvin.js\");\nconst DCP_ENV = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\nconst btoa = __webpack_require__(/*! dcp/utils/content-encoding */ \"./src/utils/content-encoding.js\");\n/**\n * Encode input to data:[<MIME-type>][;base64][;charset=<encoding>],<data>\n * @param { string | object } input \n * @param { string } [MIMEType] \n */\nexports.encodeDataURI = function utils$$encodeDataURI (input, MIMEType) {\n let inputType = typeof input;\n if (inputType === 'string' && input.startsWith('data:')) /* Already encoded, avoid double encoding */\n return input;\n let temp, encoding;\n let textType, imageType;\n if(MIMEType) {\n temp = MIMEType.split(';');\n MIMEType = temp[0];\n if(temp[1]) encoding = temp[1];\n textType = MIMEType.match(/text\\/.*/);\n textType = textType? textType[0] : [];\n imageType = MIMEType.match(/image\\/.*/);\n imageType = imageType? imageType[0] : [];\n } else {\n switch (inputType) {\n case 'string':\n MIMEType = 'text/plain';\n break;\n\n case 'boolean':\n MIMEType = 'application/json';\n break;\n\n default:\n MIMEType = 'application/x-kvin';\n break;\n }\n }\n\n switch (MIMEType) {\n case 'application/javascript': \n case 'text/plain':\n if(encoding === 'base64') {\n return 'data:text/plain;base64,' + btoa(input);\n }\n return 'data:,' + encodeURI(input);\n\n case 'application/json':\n return 'data:application/json,' + (typeof input === 'string' ? encodeURI(input) : encodeURI(JSON.stringify(input)));\n\n case 'application/x-kvin':\n return 'data:application/x-kvin,' + (typeof input === 'string' ? encodeURI(input) : encodeURI(kvin.serialize(input)));\n\n case 'application/octet-stream':\n input = new Uint8Array(input);\n return 'data:application/octet-stream;base64,' + btoa(input); \n\n case textType:\n return `data:${textType},` + encodeURI(input);\n\n case imageType:\n if (inputType === 'string') return `data:${imageType};base64,` + btoa(input);\n input = new Uint8Array(input.buffer)\n return encodeURI(`data:${imageType};base64,` + btoa(input)); \n\n default:\n throw new Error(`The content type ${MIMEType} is not supported in encodeDataURI()!`)\n }\n}\n\n// https://stackoverflow.com/questions/11089732/display-image-from-blob-using-javascript-and-websockets\n// public method for encoding an Uint8Array to base64\n\nfunction encode(input) {\n var keyStr = \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=\";\n var output = \"\";\n var chr1, chr2, chr3, enc1, enc2, enc3, enc4;\n var i = 0;\n\n while (i < input.length) {\n chr1 = input[i++];\n chr2 = i < input.length ? input[i++] : Number.NaN; // Not sure if the index \n chr3 = i < input.length ? input[i++] : Number.NaN; // checks are needed here\n\n enc1 = chr1 >> 2;\n enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);\n enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);\n enc4 = chr3 & 63;\n\n if (isNaN(chr2)) {\n enc3 = enc4 = 64;\n } else if (isNaN(chr3)) {\n enc4 = 64;\n }\n output += keyStr.charAt(enc1) + keyStr.charAt(enc2) +\n keyStr.charAt(enc3) + keyStr.charAt(enc4);\n }\n return output;\n}\n\n \n \n\n\n//# sourceURL=webpack://dcp/./src/utils/encodeDataURI.js?");
4750
4891
 
4751
4892
  /***/ }),
4752
4893
 
@@ -4778,7 +4919,7 @@ eval("/**\n * @file fetch-keystore.js\n * Utility code to fe
4778
4919
  /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
4779
4920
 
4780
4921
  "use strict";
4781
- eval("/**\n * @file fetch-uri.js\n * @author Nazila Akhavan <nazila@kingsds.network>, Wes Garland <wes@kingsds.network>\n * @date Sep 2020, Nov 2020\n *\n * Fetch URLs/ Data that is stored in the database.\n * Bootstrap some our own needs via custom MIME Types in data URLs.\n */\n\n\n/* global dcpConfig atob:writable */\n\nconst { assert } = __webpack_require__(/*! dcp/common/dcp-assert */ \"./src/common/dcp-assert.js\");\nconst kvin = __webpack_require__(/*! kvin */ \"./node_modules/kvin/kvin.js\");\nconst scopedKvin = new kvin.KVIN({Object: ({}).constructor,\n Array: ([]).constructor, \n Function: (()=>{}).constructor});\n\nconst { justFetch } = __webpack_require__(/*! ./just-fetch */ \"./src/utils/just-fetch.js\");\nconst DCP_ENV = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\n\n/** @typedef {import('dcp/common/dcp-url').DcpURL} DcpURL */\n\n/**\n * Fetch the data at a given URI and return it; data: URIs are decoded directly,\n * http or https URIs are resolved by GET queries. No data will be fetched\n * unless the origin is null, matches dcpConfig.global.allowFetchOrigins, or\n * allowedOrigins.\n *\n * @param {URL | DcpURL | string} uri - Path of the\n * file to be fetched\n * @param {string[][]} _allowedOrigins - One more arrays of URL origins that are\n * allowed. data: URIs are always allowed.\n * @returns Promise which resolves to the data given by the URI\n */\nexports.fetchURI = async function fetchURI(uri, ..._allowedOrigins) {\n const url = typeof uri === 'string' ? new URL(uri) : uri;\n let data;\n\n if (url.protocol === 'data:') {\n data = exports.parseDataURI(uri);\n } else {\n /* Implementation designed to avoid GC churn on many array concats */\n\n let allowed = false;\n\n for (let i = 1; i < arguments.length; i++) {\n const allowedOrigins = arguments[i];\n if (!Array.isArray(allowedOrigins)) {\n throw new Error('Allowed Origins are not all arrays');\n }\n // If we are fetching a file (protocol === 'file:') (ie localexec) the allow origin check is more strict - only allow\n // the file to be accessed if the path from the scheduler is EXACTLY the same as in the allowedOrigins list, not just origin.\n if (url.protocol === 'file:')\n {\n if (allowedOrigins.indexOf(url.pathname) !== -1)\n {\n allowed = true;\n break;\n }\n }\n else if (allowedOrigins.indexOf(url.origin) !== -1) {\n allowed = true;\n break;\n }\n }\n\n if (!allowed) {\n let e = new DCPError(`Not allowed to fetch from the origin '${url.origin}'.\n If fetching from a remote data set, note that workers must be allowed to fetch data from remote URLs.\n For more information on how to implement these permissions, please visit https://docs.dcp.dev/advanced/data-uri.html#worker`\n ,'EFETCH');\n \n throw e;\n }\n if (url.protocol === 'file:')\n data = (__webpack_require__(/*! fs */ \"fs\").readFileSync)(url.pathname, 'utf-8');\n else\n data = await justFetch(url, 'string', 'GET');\n }\n\n return data;\n}\n\n/**\n * Instanciate an object based on a data: uri which has properties matching\n * the parameter attributes of the uri.\n *\n * Properties:\n * - contentType: boxed string which is the content type (eg 'image/png')\n * - contentType.major: string which is the major part of the content type (eg 'image')\n * - contentType.minor: string which is the minor part of the content type (eg 'png')\n * - length: the length of the media type section of the data: URI\n * - parameters: object which holds any parameters which were specified in\n * the URI; keys are lowercased attributes.\n *\n * @note The parameters.charset property is treated a little differently to make\n * it easier on API consumers; it is *always* defined, and lower case.\n * If the charset was not specified, it is false.\n */ \nfunction MediaType(uri) {\n var mtArr;\n var mtStr = /(^data:)([^,]*)/.exec(uri)[2];\n\n assert(uri.startsWith('data:'));\n this.length = mtStr.length;\n if (mtStr === '')\n mtStr = 'text/plain;charset=US-ASCII';\n\n mtArr = mtStr.split(';');\n this.contentType = new String(mtArr.shift());\n [ this.contentType.major, this.contentType.minor ] = this.contentType.split('/');\n \n this.parameters = {}\n for (let parameter of mtArr) {\n let [ attribute, value ] = parameter.split('=');\n if (!value)\n value = true;\n this.parameters[attribute.toLowerCase()] = value;\n }\n\n if (typeof this.parameters.charset === 'undefined')\n this.parameters.charset = false;\n else\n this.parameters.charset = this.parameters.charset.toLowerCase();\n}\n\n/**\n * Parse a data: URI, returning the JavaScript value it encodes. The return type is selected\n * based on the content-type.\n *\n * <pre>\n * MIME Type Return Type\n * ------------------ ----------------------------------------------------------------------------------------------------\n * text/plain or none string primitive\n * text/* A boxed string with the contentType property set, and the charset property set if it was specified.\n * application/json whatever JSON.parse returns on the data when it is treated as a string\n * application/x-kvin whatever kvin.deserialze returns on the data when it is treated as a string\n * image/* A Uint8Array of the decoded contents with the contentType property set.\n * * A Uint8Array of the decoded contents with the contentType property set and the charset property \n * set if it was specified.\n * </pre>\n *\n * @param {string} uri the URI\n */\nexports.parseDataURI = function(uri) {\n if (typeof atob !== 'function') {\n const { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\n // eslint-disable-next-line no-global-assign\n const global = globalThis;\n global.atob = requireNative('atob');\n }\n\n var mediaType = new MediaType(uri);\n var data = uri.slice(5 + mediaType.length + 1); /* data: mediaType comma */\n\n switch(mediaType.parameters.charset)\n {\n default: {\n if (mediaType.contentType.major === 'text')\n throw new Error(`Character set ${mediaType.parameters.charset} not supported`);\n }\n case false:\n case 'utf8':\n case 'us-ascii': {\n data = mediaType.parameters.base64 ? atob(data) : decodeURI(data);\n break;\n }\n }\n \n if (mediaType.contentType == 'text/plain')\n return data;\n\n if (mediaType.contentType.major === 'text') {\n let bso = new String(data);\n if (mediaType.parameters.charset)\n bso.charset = mediaType.parameters.charset;\n return bso;\n }\n\n if (mediaType.contentType == 'application/json')\n return JSON.parse(data);\n\n if (mediaType.contentType == 'application/x-kvin')\n return scopedKvin.deserialize(data);\n\n if (mediaType.contentType.major === 'image') {\n let ui8 = Uint8Array.from(data, c => c.charCodeAt(0));\n ui8.contentType = mediaType.contentType;\n }\n\n const ui8 = Uint8Array.from(data, c => c.charCodeAt(0));\n ui8.contentType = mediaType.contentType.toString();\n if (mediaType.parameters.charset)\n ui8.charset = charset;\n return ui8;\n};\n\n\n//# sourceURL=webpack://dcp/./src/utils/fetch-uri.js?");
4922
+ eval("/**\n * @file fetch-uri.js\n * @author Nazila Akhavan <nazila@kingsds.network>, Wes Garland <wes@kingsds.network>\n * @date Sep 2020, Nov 2020\n *\n * Fetch URLs/ Data that is stored in the database.\n * Bootstrap some our own needs via custom MIME Types in data URLs.\n */\n\n\nconst { assert } = __webpack_require__(/*! dcp/common/dcp-assert */ \"./src/common/dcp-assert.js\");\nconst kvin = __webpack_require__(/*! kvin */ \"./node_modules/kvin/kvin.js\");\nconst scopedKvin = new kvin.KVIN({Object: ({}).constructor,\n Array: ([]).constructor, \n Function: (()=>{}).constructor});\n\nconst { justFetch } = __webpack_require__(/*! ./just-fetch */ \"./src/utils/just-fetch.js\");\nconst DCP_ENV = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\nconst { atob } = __webpack_require__(/*! dcp/utils/content-encoding */ \"./src/utils/content-encoding.js\");\n\n/** @typedef {import('dcp/common/dcp-url').DcpURL} DcpURL */\n\n/**\n * Fetch the data via HTTP GET at the given URI. data: URIs are decoded directly, \n * http: or https: URIs are fetched via GET queries. \n *\n * data: URIs will always be decoded.\n * Other URIs (eg https:) must have their origins listed in the allow origin list\n *\n * The return type is unpacked by justFetch - maybe not the best place for this - but this means\n * that we will return a promise that resolves to many possible JS types, dependening on the MIME\n * content-type of the response. The same observation holds for the content-type inside any data:\n * URIs.\n * \n * \n * @param {string[]} allowOriginList the allow origin list (optional, needed for non-data: URIs)\n * @param {URL | DcpURL | string} uri uri of the resource to be fetched\n * @returns If successful, promise which resolves to the data at the URI; see justFetch() for details.\n * Otherwise, promise which results to an instance of error. Errors which happen to due bad\n * origins have the code EFETCH_BAD_ORIGIN.\n */\nexports.fetchURI = async function fetchURI(uri, allowOriginList) {\n if ((typeof uri === 'object' && uri.protocol === 'data:') || uri.startsWith('data:'))\n return exports.parseDataURI(uri);\n \n const url = typeof uri === 'string' ? new URL(uri) : uri;\n \n if (url.protocol === 'file:')\n {\n if (allowOriginList.indexOf(url.pathname) === -1)\n throw new DCPError(`Not allowed to fetch from the file path '${url.pathname}'`, 'EFETCH_BAD_ORIGIN')\n \n return exports.parseFileURI(url);\n }\n \n if (allowOriginList.indexOf(url.origin) === -1)\n throw new DCPError(`Not allowed to fetch from the origin '${url.origin}'`, 'EFETCH_BAD_ORIGIN')\n \n return await justFetch(url, 'string', 'GET');\n}\n\n/**\n * @constructor\n * Instanciate an object based on a data: uri which has properties matching\n * the parameter attributes of the uri.\n * \n * @param {string} uriString The data: URI to decode\n *\n * Object Properties:\n * - contentType: boxed string which is the content type (eg 'image/png')\n * - contentType.major: string which is the major part of the content type (eg 'image')\n * - contentType.minor: string which is the minor part of the content type (eg 'png')\n * - length: the length of the media type section of the data: URI\n * - parameters: object which holds any parameters which were specified in\n * the URI; keys are lowercased attributes.\n *\n * @note The parameters.charset property is treated a little differently to make\n * it easier on API consumers; it is *always* defined, and lower case.\n * If the charset was not specified, it is false.\n */ \nexports.MediaType = function fetchUri$$MediaType(uriString) {\n var mtArr;\n var mtStr = /(^data:)([^,]*)/.exec(uriString)[2];\n\n assert(uriString.startsWith('data:'));\n this.length = mtStr.length;\n if (mtStr === '')\n mtStr = 'text/plain;charset=US-ASCII';\n\n mtArr = mtStr.split(';');\n this.contentType = new String(mtArr.shift());\n [ this.contentType.major, this.contentType.minor ] = this.contentType.split('/');\n \n this.parameters = {}\n for (let parameter of mtArr) {\n let [ attribute, value ] = parameter.split('=');\n if (!value)\n value = true;\n this.parameters[attribute.toLowerCase()] = value;\n }\n\n if (typeof this.parameters.charset === 'undefined')\n this.parameters.charset = false;\n else\n this.parameters.charset = this.parameters.charset.toLowerCase();\n \n if (this.contentType == 'application/json' || this.contentType == 'text/plain')\n {\n this.serializer = {\n method: 'json',\n parse: JSON.parse\n }\n }\n else if (this.contentType == 'application/x-kvin')\n {\n this.serializer = {\n method: 'kvin',\n parse: scopedKvin.deserialize.bind(scopedKvin)\n }\n }\n}\n\n/**\n * Estimate the number of raw bytes stored in a data URI. The estimate is pretty good for URIs\n * embedding base64. URIs which are made up of escaped characters are estimated by counting the number\n * of characters in the first part of the array, and then assuming that the encoding density is constant\n * through out. This is because the URI can be made up of bytes encoded with 1-or-3 bytes, but will \n * normally be all either almost all 1 or all 3. Mixed would be pretty rare in the wild.\n *\n * This does mean that it's possible to under-estimate by nearly 66%, so backing store should be ready\n * for this, but it's realllly unlikely to happen.\n */\nexports.estimateDataUriContentLength = function estimateDataUriContentLength(uriString, mediaType)\n{\n const headerLength = 5 + mediaType.length + 1; /* data: mediaType comma */\n\n if (!mediaType)\n mediaType = new exports.MediaType(uriString);\n if (mediaType.parameters.base64)\n return Math.ceil(((uriString.length - (mediaType.length + headerLength)) * 6) / 8);\n\n /* Assume that the distribution of escaped characters in the first bit is representative\n * of the URI as a whole. We don't want to take this apart just to estimate.\n */\n const leading = uriString.slice(mediaType.length, 1024);\n const remain = leading.replace(/%../g, '');\n const numEscs = (leading.length - remain.length);\n const bytes = remain.length + numEscs;\n const dilation = bytes / leading.length;\n \n return Math.ceil((uriString.length - headerLength) * dilation);\n}\n\n/**\n * Extract the raw data encoded a data URI.\n *\n * @param {string} uriString a valid data: URI\n * @param {exports.MediaType} mediaType optional instance of MediaType that describes this URI.\n *\n * @returns string or Buffer representing the RAW data. If the return type is a string, each character\n * in the string will represent 1 byte, 0-255, of the raw data.\n */\nexports.dataUri_extractBytes = function fetchUri$$dataUri_extractBytes(uriString, mediaType)\n{\n if (!mediaType)\n mediaType = new exports.MediaType(uriString);\n\n const payload = uriString.slice(5 + mediaType.length + 1); /* data: mediaType comma */\n const data = mediaType.parameters.base64 ? atob(payload) : unescape(payload);\n\n return data;\n}\n\n/**\n * Extract text encoded in a data URI.\n *\n * @param {string} uriString a valid data: URI\n * @param {exports.MediaType} mediaType optional instance of MediaType that describes this URI.\n *\n * @returns a JS-style Unicode string, converting from whatever charset is indicated in the\n * data URI's content-type charset attribute.\n */\nexports.dataUri_extractText = function fetchUri$$dataUri_extractText(uriString, mediaType)\n{\n if (!mediaType)\n mediaType = new exports.MediaType(uriString);\n\n switch (mediaType.parameters.charset)\n {\n default:\n throw new Error(`Character set ${mediaType.parameters.charset} not supported`);\n\n case undefined:\n case false:\n case 'iso-8859-1': case 'latin1': case 'latin-1':\n case 'us-ascii': case 'ascii':\n /* These character sets map directly onto unicode */\n return exports.dataUri_extractBytes(uriString, mediaType);\n case 'utf8': case 'utf-8':\n const payload = uriString.slice(5 + mediaType.length + 1); /* data: mediaType comma */\n return mediaType.parameters.base64 ? decodeURI(escape(atob(payload))) : decodeURI(payload);\n }\n\n throw 'unreached';\n}\n\nexports.parseFileURI = function fetchUri$$parseFileURI(uriString) {\n var fetchedResource = (__webpack_require__(/*! fs */ \"fs\").readFileSync)(uriString, 'utf-8');\n var fileExt = (__webpack_require__(/*! path */ \"./node_modules/path-browserify/index.js\").extname)(uriString.pathname);\n \n var parse;\n switch (fileExt) {\n case '.kvin':\n parse = scopedKvin.deserialize.bind(scopedKvin);\n break;\n case '.json':\n parse = JSON.parse\n break;\n }\n \n if (parse)\n fetchedResource = parse(fetchedResource);\n \n return fetchedResource;\n}\n\n/**\n * Parse a data: URI, returning the JavaScript value it encodes. The return type is selected\n * based on the content-type.\n *\n * <pre>\n * MIME Type Return Type\n * ------------------ ----------------------------------------------------------------------------------------------------\n * text/plain or none string primitive\n * text/* A boxed string with the contentType property set, and the charset property set if it was specified.\n * application/json whatever JSON.parse returns on the data when it is treated as a string\n * application/x-kvin whatever kvin.deserialze returns on the data when it is treated as a string\n * image/* A Uint8Array of the decoded contents with the contentType property set.\n * * A Uint8Array of the decoded contents with the contentType property set and the charset property \n * set if it was specified.\n * </pre>\n *\n * @param {string} uriString the URI\n */\nexports.parseDataURI = function(uriString, mediaType = null) {\n if (!mediaType)\n mediaType = new exports.MediaType(uriString);\n \n if (mediaType.contentType.major === 'text')\n {\n const unicodeString = exports.dataUri_extractText(uriString, mediaType);\n\n if (mediaType.contentType == 'text/plain')\n return unicodeString;\n\n const boxedUnicodeString = new String(unicodeString); \n if (mediaType.parameters.charset)\n boxedUnicodeString.charset = mediaType.parameters.charset;\n return boxedUnicodeString;\n }\n \n if (mediaType.serializer)\n {\n /* ALL supported serializers in DCP serialize to UTF-8 */\n const unicodeString = exports.dataUri_extractText(uriString, mediaType);\n return mediaType.serializer.parse(unicodeString);\n }\n \n let ui8;\n const rawData = exports.dataUri_extractBytes(uriString, mediaType);\n if (mediaType.contentType.major === 'image')\n {\n ui8 = Uint8Array.from(rawData, c => c.charCodeAt(0));\n ui8.contentType = mediaType.contentType;\n }\n else \n {\n ui8 = Uint8Array.from(rawData, c => c.charCodeAt(0));\n ui8.contentType = mediaType.contentType.toString();\n if (mediaType.parameters.charset)\n ui8.charset = charset;\n }\n\n return ui8;\n};\n\n\n//# sourceURL=webpack://dcp/./src/utils/fetch-uri.js?");
4782
4923
 
4783
4924
  /***/ }),
4784
4925
 
@@ -4799,7 +4940,7 @@ eval("/**\n * @file utils/http.js Helper module for things rel
4799
4940
  /***/ ((module, exports, __webpack_require__) => {
4800
4941
 
4801
4942
  "use strict";
4802
- eval("/* provided dependency */ var process = __webpack_require__(/*! ./node_modules/process/browser.js */ \"./node_modules/process/browser.js\");\n/* provided dependency */ var Buffer = __webpack_require__(/*! ./node_modules/node-polyfill-webpack-plugin/node_modules/buffer/index.js */ \"./node_modules/node-polyfill-webpack-plugin/node_modules/buffer/index.js\")[\"Buffer\"];\n/**\n * @file src/utils/index.js\n * @author Ryan Rossiter\n * @date Feb 2020\n *\n * Place to put little JS utilities. If they are more than a few lines, please `require` and `export` \n * them instead of making this monolithic.\n */\n\n\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\nconst { assert } = __webpack_require__(/*! dcp/common/dcp-assert */ \"./src/common/dcp-assert.js\");\nconst { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\nconst DCP_ENV = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\nconst schedulerConstants = __webpack_require__(/*! dcp/common/scheduler-constants */ \"./src/common/scheduler-constants.js\");\n\n module.exports.paramUtils = __webpack_require__(/*! ./assert-params */ \"./src/utils/assert-params.js\");\n module.exports.httpUtils = __webpack_require__(/*! ./http */ \"./src/utils/http.js\");\n module.exports.serialize = __webpack_require__(/*! ./serialize */ \"./src/utils/serialize.js\");\n Object.assign(exports, __webpack_require__(/*! ./web-format-date */ \"./src/utils/web-format-date.js\"));\n Object.assign(exports, __webpack_require__(/*! ./confirm-prompt */ \"./src/utils/confirm-prompt.js\"));\n Object.assign(exports, __webpack_require__(/*! ./message-to-buffer */ \"./src/utils/message-to-buffer.js\"));\n\n/** @typedef {import('dcp/dcp-client/worker/slice').Slice} Slice */\n/** @typedef {import('dcp/dcp-client/worker/sandbox').Sandbox} Sandbox */\n\n /**\n * Writes object properties into another object matching shape.\n * For example: if obj = { a: { b: 1, c: 2}}\n * and source = { a: {c: 0, d: 1}}\n * then obj will be updated to { a: { b: 1, c: 0, d: 1 } }\n * compare this to Object.assign which gives { a: { c: 0, d: 1 } }\n */\nconst setObjProps = module.exports.setObjProps = (obj, source) => {\n for (let p in source) {\n if (typeof source[p] === 'object') setObjProps(obj[p], source[p]);\n else obj[p] = source[p];\n }\n}\n\n\n/**\n * Generates a new random opaqueId i.e. a 22-character base64 string.\n * Used for job and slice ids.\n */\nmodule.exports.generateOpaqueId = function utils$$generateOpaqueId()\n{\n if (!utils$$generateOpaqueId.nanoid)\n {\n const nanoidModule = (DCP_ENV.platform === 'nodejs') ? requireNative('nanoid') : __webpack_require__(/*! nanoid */ \"./node_modules/nanoid/index.browser.js\");\n utils$$generateOpaqueId.nanoid = nanoidModule.customAlphabet('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+', schedulerConstants.workerIdLength);\n }\n\n return utils$$generateOpaqueId.nanoid();\n}\n\n/**\n * Accepts an object and a list of properties to apply to the object to retreive a final value.\n * E.g.\n * object = { a: { b: { c: 4 } } }\n * listOfProperties = [\"a\", \"b\", \"c\"]\n * return = 4\n */\nmodule.exports.getNestedFromListOfProperties = function (object, listOfProperties) {\n return listOfProperties.reduce((o, k) => {\n if (typeof o !== 'object') return undefined;\n return o[k];\n }, object);\n}\n\n/**\n * Accepts an object and a dot-separated property name to retrieve from the object.\n * E.g.\n * object = { a: { b: { c: 4 } } }\n * property = 'a.b.c'\n * return = 4\n *\n * @param {object} object \n * @param {string} property \n */\nmodule.exports.getNestedProperty = function (object, property) {\n if (typeof property !== 'string') return undefined;\n return module.exports.getNestedFromListOfProperties(object, property.split(\".\"));\n}\n\n/**\n * Accepts an object and a list of properties and a value and mutates the object\n * to have the specified value at the location denoted by the list of properties.\n * Similar to getNestedFromListOfProperties, but sets instead of gets.\n */\nmodule.exports.setNestedFromListOfProperties = function (object, listOfProperties, value) {\n if(!listOfProperties.length || listOfProperties.length < 1) {\n throw new Error(\"listOfProperties must be an array of length >= 1\");\n }\n const indexOfLastProp = listOfProperties.length - 1;\n const pathToParent = listOfProperties.slice(0, indexOfLastProp);\n const parent = module.exports.getNestedFromListOfProperties(object, pathToParent);\n if(!parent) {\n throw new Error(\"Could not find value at:\", pathToParent, \"in object:\", object);\n }\n const lastProperty = listOfProperties[indexOfLastProp];\n parent[lastProperty] = value;\n}\n\n/**\n * Block the event loop for a specified time\n * @milliseconds the number of milliseconds to wait (integer)\n */\nexports.msleep = function utils$$msleep(milliseconds) {\n try\n {\n let sab = new SharedArrayBuffer(4);\n let int32 = new Int32Array(sab);\n Atomics.wait(int32, 0, 0, milliseconds);\n }\n catch(error)\n {\n console.error('Cannot msleep;', error);\n }\n}\n\n/**\n * Block the event loop for a specified time\n * @seconds the number of seconds to wait (float)\n */\nexports.sleep = function utils$$sleep(seconds) {\n return exports.msleep(seconds * 1000);\n}\n\n/** \n * Resolve a promise after a specified time.\n * \n * @param {number} ms the number of milliseconds after which to resolve the promise.\n * @returns Promise with an extra property, intr(). Calling this function causes the promise\n * to resolve immediately. If the promise was interrupted, it will resolve with false;\n * otherwise, true.\n */\nexports.a$sleepMs = function a$sleepMs(ms)\n{\n var interrupt;\n \n const ret = new Promise((resolve, reject) => {\n var resolved = false;\n const timerHnd = setTimeout(() => { resolved=true; resolve(false) }, ms);\n function a$sleepMs_intr()\n {\n clearTimeout(timerHnd);\n if (!resolved)\n resolve(true);\n }\n \n interrupt = a$sleepMs_intr;\n });\n\n ret.intr = () => interrupt();\n return ret;\n}\n\n/** \n * @see: a$sleepMs\n * @param {number} ms the number of milliseconds after which to resolve the promise.\n */\nexports.a$sleep = function a$sleep(seconds) {\n return exports.a$sleepMs(seconds * 1000);\n}\n\n/** \n * Returns the number of millisecond in a time expression.\n * @param s {number} The number of seconds\n * @returns {number}\n */\n/**\n * @param s {string} A complex time expression using m, w, d, s, h. '10d 6h 1s' means 10 days, 6 hours, and 1 second.\n */\nexports.ms = function utils$$ms(s)\n{\n let ms = 0;\n \n if (typeof s === 'number')\n return s * 1000;\n\n assert(typeof s === 'string');\n \n for (let expr of s.match(/[0-9.]+[smhdw]/g))\n {\n let unit = expr.slice(-1);\n let value = +expr.slice(0, -1);\n\n switch(unit)\n {\n case 's': {\n ms += value * 1000;\n break;\n }\n case 'm': {\n ms += value * 1000 * 60;\n break;\n }\n case 'h': {\n ms += value * 1000 * 60 * 60;\n break;\n }\n case 'd': {\n ms = value * 1000 * 60 * 60 * 24;\n break;\n }\n case 'w': {\n ms = value * 1000 * 60 * 60 * 24 * 7;\n break;\n }\n default: {\n throw new Error(`invalid time unit ${unit}`);\n }\n }\n }\n\n return ms;\n}\n\n/**\n * Returns a percentage as a number.\n * @param n {number} this number is returned\n */\n/**\n * @param n {string} this string is converted to a number and returned; it is divided by 100 if it ends in %.\n */\nexports.pct = function utils$$pct(n)\n{\n if (typeof n === 'number')\n return n / 100;\n\n if (n.match(/%$/))\n return +n / 100;\n\n return +n;\n}\n\n/**\n * Coerce human-written or registry-provided values into Boolean in a sensisble way.\n */\nexports.booley = function utils$$booley(check)\n{\n switch (typeof check)\n {\n case 'undefined':\n return false;\n case 'string':\n return check && (check !== 'false');\n case 'boolean':\n return check;\n case 'number':\n case 'object':\n return Boolean(check);\n default:\n throw new Error(`can't coerce ${typeof check} to booley`);\n }\n}\n\ntry {\n exports.useChalk = requireNative ? requireNative('tty').isatty(0) || process.env.FORCE_COLOR : false;\n}\ncatch (error) {\n if (error.message.includes('no native require'))\n exports.useChalk = false;\n else\n throw error;\n}\n\n/** \n * Factory function which constructs an error message relating to version mismatches\n * @param {string} oldThingLabel The name of the thing that is too old\n * @param {string} upgradeThingLabel [optional] The name of the thing that needs to be upgraded to fix that; if\n * unspecified, we use oldThingLabel.\n * @param {string} newThingLabel What the thing is that is that is newer than oldThingLabel\n * @param {string} minimumVersion The minimum version of the old thing that the new thing supports. \n * Obstensibibly semver, but unparsed.\n * @param {string} code [optional] A code property to add to the return value\n *\n * @returns instance of Error\n */\nexports.versionError = function dcpClient$$versionError(oldThingLabel, upgradeThingLabel, newThingLabel, minimumVersion, code)\n{\n function repeat(what, len) {\n let out = '';\n while (len--)\n out += what;\n return out;\n }\n\n function bold(string) {\n if ((__webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\").platform) === 'nodejs') {\n const chalk = new requireNative('chalk').constructor({enabled: exports.useChalk});\n\n return chalk.yellow(string);\n }\n\n return string;\n }\n \n var mainMessage = `${oldThingLabel} is too old; ${newThingLabel} needs version ${minimumVersion}`;\n var upgradeMessage = bold(`Please upgrade ${upgradeThingLabel || oldThingLabel}${repeat(\" \", mainMessage.length - 15 - upgradeThingLabel.length)}`);\n var error = new Error(`\n****${repeat('*', mainMessage.length)}****\n*** ${repeat(' ', mainMessage.length)} ***\n*** ${mainMessage} ***\n*** ${upgradeMessage} ***\n*** ${repeat(' ', mainMessage.length)} ***\n****${repeat('*', mainMessage.length)}****\n`);\n\n if (code)\n error.code = code;\n return error;\n}\n\n/**\n * Perf Timers:\n * shortTime\n * class PerfTimers\n *********************************************************************************************/\n\n/**\n * Previous short form perf time stamp.\n * @private\n * @type {Date}\n */\nlet previousTime = new Date();\n\n/**\n * Short form perf time format with timespan diff from last call.\n * @param {boolean} [displayDiffOnly=true]\n * @returns {string}\n */\nexports.shortTime = function util$$shortTime(displayDiffOnly=true) {\n const currentTime = new Date();\n const diff = currentTime.getTime() - previousTime.getTime();\n previousTime = currentTime;\n if (displayDiffOnly) return `diff=${diff}`;\n return `diff=${diff}: ${currentTime.getMinutes()}.${currentTime.getSeconds()}.${currentTime.getMilliseconds()}`;\n}\n\n/**\n * By default returns\n * functionName:lineNumber: message\n * When (!message || !message.length), returns\n * functionName:lineNumber\n *\n * When shortForm is true, returns\n * lineNumber: message\n *\n * @param {string} [message]\n * @param {boolean} [shortForm=false]\n * @returns {string}\n */\nexports.debugLine = function debugLine(message, depth = 2, shortForm = false) {\n const e = new Error();\n const frame = e.stack.split(\"\\n\")[depth];\n const lineNumber = frame.split(\":\").reverse()[1];\n const funcName = frame.split(\" \")[5];\n const header = shortForm ? 'line#' : funcName;\n const headerAndLine = `${header}:${lineNumber}`;\n if (!message || !message.length) return headerAndLine;\n return headerAndLine + \" \" + message;\n}\n\n/**\n * class Tracer: Method mark, displays time span since the last mark.\n *\n * When perf testing the first step is to place a bunch of timer entries\n * in the code where the time difference from the last entry is displayed\n * This class Tracer is used as follows:\n * const tracer = new Tracer(tag);\n * ...............................\n * tracer.mark(); // displays dt1: lineNumber: tag -- where dt1 is time since constructor was called\n * ...............................\n * tracer.mark(); // displays dt2: lineNumber: tag -- where dt2 is time since previous mark\n * ...............................\n * tracer.mark(); // displays dt3: lineNumber: tag -- where dt3 is time since previous mark\n *\n * There are many variations possible as can be seen by the comment on the mark method.\n */\nclass Tracer {\n /**\n * \n * @param {boolean} [shortForm=true]\n * @param {string} [tag]\n */\n constructor(shortForm = true, tag) {\n this.shortForm = shortForm;\n this.tag = tag;\n this.previousTime = new Date();\n }\n /**\n * Let diff be the time span since the last call to mark or the Tracer constructor.\n * Let lineNumber be the line # of the file containing the call to mark.\n * Let line# be the string \"line#\"\n * Let funcName be the name of the function where mark was called.\n * When tag.length > 0,\n * displays: diff: line#:lineNumber: tag:message\n * or: diff: line#:lineNumber: tag when !message\n * or: diff: funcName:lineNumber tag:message when !shortForm\n * or: diff: funcName:lineNumber tag when !shortForm and !message\n * else\n * displays: diff: line#:lineNumber: message\n * or: diff: line#lineNumber: when !message\n * or: diff: funcName:lineNumber message when !shortForm\n * or: diff: funcName:lineNumber when !shortForm and !message\n * @param {string} [message]\n */\n mark(message) {\n const currentTime = new Date();\n const diff = currentTime.getTime() - this.previousTime.getTime();\n this.previousTime = currentTime;\n let tag = this.tag;\n if (tag && tag.length && message) tag = `${tag}:${message}`;\n else if (message) tag = message;\n console.log(`${diff}: ${exports.debugLine(tag, 3 /* depth*/, this.shortForm)}`);\n }\n}\nexports.Tracer = Tracer;\n\n/**\n* class PerfTimer: Construct a set of possibly overlapping timers.\n*\n* Example:\n* const perfTimers = new PerfTimers(['timer0', 'timer1', 'timer2']);\n* perfTimers.start('timer0');\n* perfTimers.start('timer1');\n* .........................\n* perfTimers.stop('timer1');\n* .........................\n* perfTimers.stop('timer0');\n* .........................\n* perfTimers.start('timer2');\n* ..........................\n* perfTimers.stop('timer2');\n* perfTimers.sum(name => name !== 'timer1');\n*\n* Should display something like:\n* timer1: 472ms\n* timer0: 1650ms\n* timer2: 2333ms\n* The sum of timers [timer0, timer2] is 3983ms\n*/\nclass PerfTimers {\n /**\n * @constructor\n * @param {string[]} perfTimerNames\n * @param {boolean} [disable=false]]\n * @param {boolean} [summaryView=false]]\n */\n constructor (perfTimerNames, disable = false, summaryView = false) {\n this.disable = disable;\n if (this.disable) return;\n assert(Array.isArray(perfTimerNames));\n /** @type {string[]} */\n this.perfTimerNames = perfTimerNames;\n /** @type {boolean} */\n this.summaryView = summaryView;\n /** @type {number[]} */\n this.perfTimers = Array.from({ length: perfTimerNames.length }, (v, i) => 0);\n /** @type {object} */\n this.perfTimerMap = {};\n for (let k = 0; k < this.perfTimerNames.length; k++)\n this.perfTimerMap[this.perfTimerNames[k]] = k;\n }\n /**\n * Start a timer on perfTimerName\n * @param {string} perfTimerName\n */\n start(perfTimerName) {\n if (this.disable) return;\n const slot = this.perfTimerMap[perfTimerName];\n if (slot === undefined) throw new Error(`PerfDebugging: perfTimer '${perfTimerName}' not found.`);\n this.perfTimers[slot] = Date.now();\n }\n /**\n * Stop a timer on perfTimerName and display the result.\n * @param {string} perfTimerName\n */\n stop(perfTimerName) {\n if (this.disable) return;\n const slot = this.perfTimerMap[perfTimerName];\n if (slot === undefined) throw new Error(`PerfDebugging: perfTimer '${perfTimerName}' not found.`);\n this.perfTimers[slot] = Date.now() - this.perfTimers[slot];\n if (!this.summaryView) console.log(`${perfTimerName}: ${this.perfTimers[slot]}ms`);\n }\n /** \n * @callback PredicateCallback\n * @param {string} value\n * @param {number} index\n * @param {string[]} array\n */\n /**\n * Use predicate to choose a subset of this.perfTimerNames,\n * sum up the corresponding timers and display the result.\n * @param {PredicateCallback} predicate\n * @param {boolean} [forceDisplay=false]\n */\n sum(predicate, forceDisplay = false) {\n if (this.disable || this.summaryView && !forceDisplay) return;\n const names = this.perfTimerNames.filter(predicate);\n const timers = this.perfTimers.map(k => this.perfTimerMap(names[k]));\n const sum = timers.reduce((previous, current) => previous + current, 0);\n console.log(`The sum of timers ${JSON.stringify(names)} is ${sum}ms`);\n }\n /**\n * Display summary.\n * @param {PredicateCallback} predicate\n * @param {boolean} [forceDisplay=false]\n */\n summary(predicate, forceDisplay = false) {\n if (this.disable) return;\n if (this.summaryView || forceDisplay) {\n for (let k = 0; k < this.perfTimers.length; k++)\n console.log(`${this.perfTimerNames[k]}: ${this.perfTimers[k]}ms`);\n }\n if (predicate) this.sum(predicate);\n }\n}\nexports.PerfTimers = PerfTimers;\n\n/**\n * Sandbox and Slice debugging and logging tools.\n * dumpSlices -- dump array of slices\n * dumpSandboxes -- dump array of sandboxes\n * dumpSlicesIfNotUnique -- dump array of slices when there are dups\n * dumpSandboxesIfNotUnique -- dump array of sandboxes when there are dups\n * isUniqueSlices -- detect dups in an array of slices\n * isUniqueSandboxes -- detect dups in an array of sandboxes\n *********************************************************************************************/\n\n/**\n * Log sliceArray.\n * @param {Slice[]} sliceArray\n * @param {string} [header]\n */\nexports.dumpSlices = function utils$$dumpSlices(sliceArray, header) {\n if (header) console.log(`\\n${header}`);\n console.log(exports.compressSlices(sliceArray));\n}\n\n/**\n * Log sandboxArray.\n * @param {Sandbox[]} sandboxArray\n * @param {string} [header]\n */\nexports.dumpSandboxes = function utils$$dumpSandboxes(sandboxArray, header) {\n if (header) console.log(`\\n${header}`);\n console.log(exports.compressSandboxes(sandboxArray));\n}\n\n/**\n * If the elements of sliceArray are not unique, log the duplicates and log the full array.\n * @param {Slice[]} sliceArray\n * @param {string} [header]\n */\nexports.dumpSlicesIfNotUnique = function utils$$dumpSlicesIfNotUnique(sliceArray, header) {\n if (!exports.isUniqueSlices(sliceArray, header))\n console.log(exports.compressSlices(sliceArray));\n}\n\n/**\n * If the elements of sandboxArray are not unique, log the duplicates and log the full array.\n * @param {Sandbox[]} sandboxArray\n * @param {string} [header]\n */\nexports.dumpSandboxesIfNotUnique = function utils$$dumpSandboxesIfNotUnique(sandboxArray, header) {\n if (!exports.isUniqueSandboxes(sandboxArray, header))\n console.log(exports.compressSandboxes(sandboxArray));\n}\n\n/**\n * Checks whether the elements of sliceArray are unique and if not, log the duplicates.\n * @param {Slice[]} sliceArray\n * @param {string} [header]\n * @param {function} [log]\n * @returns {boolean}\n */\nexports.isUniqueSlices = function utils$$isUniqueSlices(sliceArray, header, log) {\n const slices = [];\n let once = true;\n sliceArray.forEach(x => {\n if (slices.indexOf(x) >= 0) {\n if (once && header) console.log(`\\n${header}`); once = false;\n log ? log(x) : console.log(`\\tFound duplicate slice ${x.identifier}.`);\n } else slices.push(x);\n });\n return sliceArray.length === slices.length;\n}\n\n/**\n * Checks whether the elements of sandboxArray are unique and if not, log the duplicates.\n * @param {Sandbox[]} sandboxArray\n * @param {string} [header]\n * @param {function} [log]\n * @returns {boolean}\n */\nexports.isUniqueSandboxes = function utils$$isUniqueSandboxes(sandboxArray, header, log) {\n const sandboxes = [];\n let once = true;\n sandboxArray.forEach(x => {\n if (sandboxes.indexOf(x) >= 0) {\n if (once && header) console.log(`\\n${header}`); once = false;\n log ? log(x) : console.log(`\\tFound duplicate sandbox ${x.identifier}.`);\n } else sandboxes.push(x);\n });\n return sandboxArray.length === sandboxes.length;\n}\n\n/**\n * JSON Serialization\n * stringify -- ignore cycles\n * dumpJSON -- safely wrapped stringify with header\n * dumpObject -- Apply dumpJSON to every [key, value] of Object.entries(theObject)\n *********************************************************************************************/\n\n/**\n * Quck and dirty JSON serialization that ignores cycles.\n *\n * @param {*} theAnything - entity to be serialized.\n * @param {number} [truncationLength=512] - number of string elements to return.\n * @returns {string}\n */\nexports.stringify = function _stringify(theAnything, truncationLength = 512) {\n let cache = [];\n const str = JSON.stringify(theAnything, (key, value) => {\n if (typeof value === 'object' && value !== null) {\n if (cache.includes(value)) return;\n cache.push(value);\n }\n return value;\n });\n cache = null;\n return str ? str.slice(0, truncationLength) : null;\n}\n\n/**\n * Calls truncated JSON.stringify on theAnything.\n * @param {*} theAnything\n * @param {string} [header='dumpJSON']\n * @param {number} [truncationLength=512]\n */\nexports.dumpJSON = function utils$$dumpJSON(theAnything, header = 'dumpJSON: ', truncationLength = 512) {\n if (theAnything) {\n const strV = exports.stringify(theAnything, truncationLength);\n if (strV) console.log(`${header}: ${String(strV)}`);\n else console.log(`${header}:`, theAnything);\n } else console.log(`${header}:`, theAnything);\n}\n\n/**\n * Iterates over all property [key, value]-pairs of theObject and call truncated JSON.stringify on property values.\n * @param {object} theObject\n * @param {string} [header='dumpObject']\n * @param {number} [truncationLength=512]\n */\nexports.dumpObject = function utils$$dumpObject(theObject, header = 'dumpObject: ', truncationLength = 512, dumpKeys = true) {\n if (theObject) {\n if (dumpKeys) console.log(`${header}: dump the keys`, Object.keys(theObject));\n console.log(`${header}: dump the key-value entries...`);\n console.group();\n for (const [key, value] of Object.entries(theObject))\n exports.dumpJSON(value, `${header}.${key}`, truncationLength);\n console.groupEnd();\n }\n}\n\n/**\n * Compressed Representations and Maps\n * toJobMap\n * compressSandboxes\n * compressSlices\n * compressJobMap\n * compressJobArray\n * compressJobValue\n * compressJobEntry\n * compressEnhancedJobEntry\n * truncateAddress\n * compressRange\n * compressEnhancedRange\n *********************************************************************************************/\n\n/**\n * @param {object[]} jobArray\n * @param {function} functor\n * @returns {object}\n */\nexports.toJobMap = function utils$$toJobMap(jobArray, functor) {\n const jobMap = {};\n for (const x of jobArray) {\n if (!jobMap[x.jobAddress]) jobMap[x.jobAddress] = [functor(x)];\n else jobMap[x.jobAddress].push(functor(x));\n }\n return jobMap;\n}\n\n/**\n * @param {Sandbox[]} sandboxArray\n * @param {number} [digits=-1]\n * @returns {string}\n */\nexports.compressSandboxes = function utils$$compressSandboxes(sandboxArray, digits = -1) {\n const jobSandboxMap = exports.toJobMap(sandboxArray, sbx => sbx.id);\n return exports.compressJobMap(jobSandboxMap, false /* skipFirst*/, digits);\n}\n\n/**\n * @param {Slice[]} sliceArray\n * @param {number} [digits=-1]\n * @returns {string}\n */\nexports.compressSlices = function utils$$compressSlices(sliceArray, digits = -1) {\n const jobSliceMap = exports.toJobMap(sliceArray, slice => slice.sliceNumber);\n return exports.compressJobMap(jobSliceMap, false /* skipFirst*/, digits);\n}\n\n/**\n * @param {object} jobMap\n * @param {boolean} [skipFirst=false]\n * @param {number} [digits=-1]\n * @returns {string}\n */\nexports.compressJobMap = function utils$$compressJobMap(jobMap, skipFirst = false, digits = -1) {\n return exports.compressJobArray(Object.entries(jobMap), skipFirst, digits);\n}\n\n/**\n * @param {object[]} jobArray\n * @param {boolean} [skipFirst=false]\n * @param {number} [digits=-1]\n * @returns {string}\n */\nexports.compressJobArray = function utils$$compressJobArray(jobArray, skipFirst = false, digits = -1) {\n let output = '';\n for (let k = 0; k < jobArray.length; k++) {\n output += exports.compressJobValue(jobArray[k], skipFirst, digits);\n }\n return output;\n}\n\n/**\n * @param {object|object[]} jobValue\n * @param {boolean} [skipFirst=false]\n * @param {number} [digits=-1]\n * @returns {string}\n */\nexports.compressJobValue = function utils$$compressJobValue(jobValue, skipFirst = false, digits = -1) {\n if (jobValue.job && jobValue.slices)\n return exports.compressJobEntry(jobValue.job, (skipFirst ? jobValue.slices.slice(1) : jobValue.slices), digits);\n if (jobValue.length === 2)\n return exports.compressJobEntry(jobValue[0], (skipFirst ? jobValue[1].slice(1) : jobValue[1]), digits);\n return 'nada';\n}\n\n/**\n * @param {*} jobAddress\n * @param {Array<number|Number>} sliceNumbers\n * @param {number} [digits=-1]\n * @returns {string}\n */\nexports.compressJobEntry = function utils$$compressJobEntry(jobAddress, sliceNumbers, digits = -1) {\n return `${exports.truncateAddress(jobAddress, digits)}:[${exports.compressRange(sliceNumbers)}]:`;\n}\n\nexports.compressEnhancedJobEntry = function utils$$compressEnhancedJobEntry(job, slices, digits = -1) {\n return `${job.id}.${exports.truncateAddress(job.address, digits)}:[${exports.compressEnhancedRange(slices)}]:`;\n}\n\n/**\n * @param {*} address\n * @param {number} [digits=-1]\n * @returns {string}\n */\nexports.truncateAddress = function utils$$truncateAddress(address, digits = -1) {\n let value = address.toString();\n if (digits < 0) return value.startsWith('0x') ? value.substring(2) : fragment;\n return value.startsWith('0x') ? value.substring(2, 2 + digits) : value.substring(0, digits);\n}\n\n/**\n * Input [2, 3, 4, 7, 5, 8, 9, 13, 14, 15] returns '2-5,7-9,13-15'\n * Input [2, 3, 4, 7, 5, 8, 9, 13] returns '2-5,7-9,13'\n * Input [2, 3, 4, 7, 5, 4, 4, 8, 9] returns '2-4,4,4-5,7-9'\n * @param {Array<number|Number>} numberArray\n * @returns {string}\n */\nexports.compressRange = function utils$$compressRange(numberArray) {\n assert(numberArray && Array.isArray(numberArray));\n numberArray.sort((x, y) => x - y); // increasing...\n let start = numberArray[0];\n let output = `${start}`;\n for (let k = 1; k < numberArray.length; k++) {\n assert(typeof numberArray[k] === 'number' || numberArray[k] && numberArray[k].constructor.name === 'Number');\n if (numberArray[k] - numberArray[k - 1] !== 1) {\n output += (numberArray[k - 1] > start) ? `-${numberArray[k - 1]},` : ',';\n start = numberArray[k];\n output += `${start}`;\n } else if (k === numberArray.length - 1) {\n output += `-${numberArray[k]}`;\n }\n }\n return output;\n}\n\nexports.compressEnhancedRange = function utils$$compressEnhancedRange(slices) {\n assert(slices && Array.isArray(slices));\n slices.sort((x, y) => x.sliceNumber - y.sliceNumber); // increasing...\n let start = slices[0];\n let output = fragment(start);\n for (let k = 1; k < slices.length; k++) {\n if (slices[k].sliceNumber - slices[k - 1].sliceNumber !== 1) {\n output += (slices[k - 1].sliceNumber > start.sliceNumber) ? `-${fragment(slices[k - 1])},` : ',';\n start = slices[k];\n output += `${fragment(start)}`;\n } else if (k === slices.length - 1) {\n output += `-${fragment(slices[k])}`;\n }\n }\n return output;\n}\n\nfunction fragment(slice) {\n return `${slice.sliceNumber}.${slice.isEstimationSlice}.${slice.isLongSlice}`;\n}\n\n/**\n * shuffle\n * hashGeneration\n *********************************************************************************************/\n\nexports.shuffle = function utils$$shuffle(jobDescriptors, partitionPortions) {\n let jobDescriptorsInEstimation = [];\n let partitionPortionsInEstimation = [];\n for (let [index, jobDescriptor] of jobDescriptors.entries()) {\n if(jobDescriptor.job.status === schedulerConstants.jobStatus.estimation) {\n jobDescriptorsInEstimation.push(jobDescriptor);\n jobDescriptors.splice(index, 1);\n partitionPortionsInEstimation.push(partitionPortions[index]);\n partitionPortions.splice(index, 1) \n }\n }\n \n let currentIndex = jobDescriptors.length, randomIndex;\n // While there remain elements to shuffle...\n while (0 !== currentIndex && jobDescriptors.length > 0) {\n\n // Pick a remaining element...\n randomIndex = Math.floor(Math.random() * currentIndex);\n currentIndex--;\n\n // And swap it with the current element.\n [jobDescriptors[currentIndex], jobDescriptors[randomIndex]] = [\n jobDescriptors[randomIndex], jobDescriptors[currentIndex]];\n\n [partitionPortions[currentIndex], partitionPortions[randomIndex]] = [\n partitionPortions[randomIndex], partitionPortions[currentIndex]];\n }\n \n return [\n [...jobDescriptorsInEstimation, ...jobDescriptors],\n [...partitionPortionsInEstimation, ...partitionPortions]\n ]\n}\n\nexports.hashGeneration = function utils$$hashGeneration(object) {\n const { sha256 } = (__webpack_require__(/*! dcp/dcp-client/wallet/keystore */ \"./src/dcp-client/wallet/keystore.js\")._internalEth.util);\n return sha256(Buffer.from(JSON.stringify(object)));\n}\n\n/**\n * CG service error handling types and helpers.\n *********************************************************************************************/\n\n/**\n * @typedef {object} apiClientType\n * @property {boolean} success\n * @property {*} [payload]\n * @property {DCPError} [error]\n */\n\n/**\n * @typedef {object} apiServiceType\n * @property {boolean} success\n * @property {*} [payload]\n * @property {string} [message]\n * @property {string} [stack]\n * @property {string} [code]\n */\n\n/**\n * @param {apiServiceType} payload \n * @returns {apiClientType}\n */\nexports.reconstructServiceError = function utils$$reconstructServiceError(payload) {\n assert(!payload.success);\n let ex = new DCPError(payload.message);\n ex.stack = dcpConfig.worker.allowConsoleAccess ? payload.stack : '';\n if (payload.code) ex.code = payload.code;\n return exports._clientError(ex);\n}\n\n/**\n * @param {DCPError} ex\n * @returns {apiClientType}\n */\nexports._clientError = function utils$$_clientError(ex) {\n if (dcpConfig.worker.allowConsoleAccess) console.error(ex);\n return { success: false, error: ex };\n}\n/**\n * @param {string} message\n * @returns {apiClientType}\n */\nexports.clientError = function utils$$clientError(message) {\n const ex = new DCPError(message);\n return exports._clientError(ex);\n}\n\n/**\n * @param {DCPError} ex\n * @returns {apiServiceType}\n */\nexports._serviceError = function utils$$_serviceError(ex) {\n return { success: false, message: ex.message, stack: ex.stack, code: ex.code };\n}\n/**\n * @param {string} message\n * @param {string|object} codeEx\n * @returns {apiServiceType}\n */\nexports.serviceError = function utils$$serviceError(message, codeEx) {\n const ex = new DCPError(message, codeEx);\n return exports._serviceError(ex);\n}\n\n/**\n * exports\n *********************************************************************************************/\n\nif ((__webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\").platform) === 'nodejs') {\n // Assigning the properties explicitly for VSCode intellisense.\n const tmpFiles = __webpack_require__(/*! ./tmpfiles */ \"./src/utils/tmpfiles.js\");\n exports.catFile = tmpFiles.catFile;\n exports.createTempFile = tmpFiles.createTempFile;\n exports.systemTempDir = tmpFiles.systemTempDir;\n\n Object.assign(exports, __webpack_require__(/*! ./readln */ \"./src/utils/readln.js\"));\n}\nObject.assign(exports, __webpack_require__(/*! ./sh */ \"./src/utils/sh.js\"));\nObject.assign(exports, __webpack_require__(/*! ./eventUtils */ \"./src/utils/eventUtils.js\"));\nObject.assign(exports, __webpack_require__(/*! ./obj-merge */ \"./src/utils/obj-merge.js\"));\nObject.assign(exports, __webpack_require__(/*! ./make-data-uri */ \"./src/utils/make-data-uri.js\"));\nObject.assign(exports, __webpack_require__(/*! ./just-fetch */ \"./src/utils/just-fetch.js\"));\nObject.assign(exports, __webpack_require__(/*! ./inventory */ \"./src/utils/inventory.js\"));\nObject.assign(exports, __webpack_require__(/*! ./fetch-keystore */ \"./src/utils/fetch-keystore.js\"));\n\nmodule.exports.fetchURI = __webpack_require__(/*! ./fetch-uri */ \"./src/utils/fetch-uri.js\").fetchURI;\nmodule.exports.encodeDataURI = __webpack_require__(/*! ./encodeDataURI */ \"./src/utils/encodeDataURI.js\").encodeDataURI;\n\n\n//# sourceURL=webpack://dcp/./src/utils/index.js?");
4943
+ eval("/* provided dependency */ var process = __webpack_require__(/*! ./node_modules/process/browser.js */ \"./node_modules/process/browser.js\");\n/* provided dependency */ var Buffer = __webpack_require__(/*! ./node_modules/node-polyfill-webpack-plugin/node_modules/buffer/index.js */ \"./node_modules/node-polyfill-webpack-plugin/node_modules/buffer/index.js\")[\"Buffer\"];\n/**\n * @file src/utils/index.js\n * @author Ryan Rossiter\n * @date Feb 2020\n *\n * Place to put little JS utilities. If they are more than a few lines, please `require` and `export` \n * them instead of making this monolithic.\n */\n\n\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\nconst { assert } = __webpack_require__(/*! dcp/common/dcp-assert */ \"./src/common/dcp-assert.js\");\nconst { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\nconst DCP_ENV = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\n\nmodule.exports.paramUtils = __webpack_require__(/*! ./assert-params */ \"./src/utils/assert-params.js\");\nmodule.exports.httpUtils = __webpack_require__(/*! ./http */ \"./src/utils/http.js\");\nmodule.exports.serialize = __webpack_require__(/*! ./serialize */ \"./src/utils/serialize.js\");\nObject.assign(exports, __webpack_require__(/*! ./web-format-date */ \"./src/utils/web-format-date.js\"));\nObject.assign(exports, __webpack_require__(/*! ./confirm-prompt */ \"./src/utils/confirm-prompt.js\"));\nObject.assign(exports, __webpack_require__(/*! ./message-to-buffer */ \"./src/utils/message-to-buffer.js\"));\nObject.assign(exports, __webpack_require__(/*! ./content-encoding */ \"./src/utils/content-encoding.js\"));\n\n/** @typedef {import('dcp/dcp-client/worker/slice').Slice} Slice */\n/** @typedef {import('dcp/dcp-client/worker/sandbox').Sandbox} Sandbox */\n\n /**\n * Writes object properties into another object matching shape.\n * For example: if obj = { a: { b: 1, c: 2}}\n * and source = { a: {c: 0, d: 1}}\n * then obj will be updated to { a: { b: 1, c: 0, d: 1 } }\n * compare this to Object.assign which gives { a: { c: 0, d: 1 } }\n */\nconst setObjProps = module.exports.setObjProps = (obj, source) => {\n for (let p in source) {\n if (typeof source[p] === 'object') setObjProps(obj[p], source[p]);\n else obj[p] = source[p];\n }\n}\n\n\n/**\n * Generates a new random opaqueId i.e. a 22-character base64 string.\n * Used for job and slice ids.\n */\nmodule.exports.generateOpaqueId = function utils$$generateOpaqueId()\n{\n const schedulerConstants = __webpack_require__(/*! dcp/common/scheduler-constants */ \"./src/common/scheduler-constants.js\");\n if (!utils$$generateOpaqueId.nanoid)\n {\n const nanoidModule = (DCP_ENV.platform === 'nodejs') ? requireNative('nanoid') : __webpack_require__(/*! nanoid */ \"./node_modules/nanoid/index.browser.js\");\n utils$$generateOpaqueId.nanoid = nanoidModule.customAlphabet('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+', schedulerConstants.workerIdLength);\n }\n\n return utils$$generateOpaqueId.nanoid();\n}\n\n/**\n * Accepts an object and a list of properties to apply to the object to retreive a final value.\n * E.g.\n * object = { a: { b: { c: 4 } } }\n * listOfProperties = [\"a\", \"b\", \"c\"]\n * return = 4\n */\nmodule.exports.getNestedFromListOfProperties = function (object, listOfProperties) {\n return listOfProperties.reduce((o, k) => {\n if (typeof o !== 'object') return undefined;\n return o[k];\n }, object);\n}\n\n/**\n * Accepts an object and a dot-separated property name to retrieve from the object.\n * E.g.\n * object = { a: { b: { c: 4 } } }\n * property = 'a.b.c'\n * return = 4\n *\n * @param {object} object \n * @param {string} property \n */\nmodule.exports.getNestedProperty = function (object, property) {\n if (typeof property !== 'string') return undefined;\n return module.exports.getNestedFromListOfProperties(object, property.split(\".\"));\n}\n\n/**\n * Accepts an object and a list of properties and a value and mutates the object\n * to have the specified value at the location denoted by the list of properties.\n * Similar to getNestedFromListOfProperties, but sets instead of gets.\n */\nmodule.exports.setNestedFromListOfProperties = function (object, listOfProperties, value) {\n if(!listOfProperties.length || listOfProperties.length < 1) {\n throw new Error(\"listOfProperties must be an array of length >= 1\");\n }\n const indexOfLastProp = listOfProperties.length - 1;\n const pathToParent = listOfProperties.slice(0, indexOfLastProp);\n const parent = module.exports.getNestedFromListOfProperties(object, pathToParent);\n if(!parent) {\n throw new Error(\"Could not find value at:\", pathToParent, \"in object:\", object);\n }\n const lastProperty = listOfProperties[indexOfLastProp];\n parent[lastProperty] = value;\n}\n\n/**\n * Block the event loop for a specified time\n * @milliseconds the number of milliseconds to wait (integer)\n */\nexports.msleep = function utils$$msleep(milliseconds) {\n try\n {\n let sab = new SharedArrayBuffer(4);\n let int32 = new Int32Array(sab);\n Atomics.wait(int32, 0, 0, milliseconds);\n }\n catch(error)\n {\n console.error('Cannot msleep;', error);\n }\n}\n\n/**\n * Block the event loop for a specified time\n * @seconds the number of seconds to wait (float)\n */\nexports.sleep = function utils$$sleep(seconds) {\n return exports.msleep(seconds * 1000);\n}\n\n/** \n * Resolve a promise after a specified time.\n * \n * @param {number} ms the number of milliseconds after which to resolve the promise.\n * @returns Promise with an extra property, intr(). Calling this function causes the promise\n * to resolve immediately. If the promise was interrupted, it will resolve with false;\n * otherwise, true.\n */\nexports.a$sleepMs = function a$sleepMs(ms)\n{\n var interrupt;\n \n const ret = new Promise((resolve, reject) => {\n var resolved = false;\n const timerHnd = setTimeout(() => { resolved=true; resolve(false) }, ms);\n function a$sleepMs_intr()\n {\n clearTimeout(timerHnd);\n if (!resolved)\n resolve(true);\n }\n \n interrupt = a$sleepMs_intr;\n });\n\n ret.intr = () => interrupt();\n return ret;\n}\n\n/** \n * @see: a$sleepMs\n * @param {number} ms the number of milliseconds after which to resolve the promise.\n */\nexports.a$sleep = function a$sleep(seconds) {\n return exports.a$sleepMs(seconds * 1000);\n}\n\n/** \n * Returns the number of millisecond in a time expression.\n * @param s {number} The number of seconds\n * @returns {number}\n */\n/**\n * @param s {string} A complex time expression using m, w, d, s, h. '10d 6h 1s' means 10 days, 6 hours, and 1 second.\n */\nexports.ms = function utils$$ms(s)\n{\n let ms = 0;\n \n if (typeof s === 'number')\n return s * 1000;\n\n assert(typeof s === 'string');\n \n for (let expr of s.match(/[0-9.]+[smhdw]/g))\n {\n let unit = expr.slice(-1);\n let value = +expr.slice(0, -1);\n\n switch(unit)\n {\n case 's': {\n ms += value * 1000;\n break;\n }\n case 'm': {\n ms += value * 1000 * 60;\n break;\n }\n case 'h': {\n ms += value * 1000 * 60 * 60;\n break;\n }\n case 'd': {\n ms = value * 1000 * 60 * 60 * 24;\n break;\n }\n case 'w': {\n ms = value * 1000 * 60 * 60 * 24 * 7;\n break;\n }\n default: {\n throw new Error(`invalid time unit ${unit}`);\n }\n }\n }\n\n return ms;\n}\n\n/**\n * @param n {number} this number is returned, divided by 100 .\n */\n/**\n * @param n {string} this string is converted to a number and returned; it is divided by 100 if it ends in %.\n */\n/**\n * Returns a percentage as a number.\n * @param n {number|string} this number is returned\n * @returns {number}\n */\nexports.pct = function utils$$pct(n)\n{\n if (typeof n === 'number')\n return n / 100;\n\n if (n.match(/%$/))\n return +n / 100;\n\n return +n;\n}\n\n/**\n * Coerce human-written or registry-provided values into Boolean in a sensisble way.\n */\nexports.booley = function utils$$booley(check)\n{\n switch (typeof check)\n {\n case 'undefined':\n return false;\n case 'string':\n return check && (check !== 'false');\n case 'boolean':\n return check;\n case 'number':\n case 'object':\n return Boolean(check);\n default:\n throw new Error(`can't coerce ${typeof check} to booley`);\n }\n}\n\ntry {\n exports.useChalk = requireNative ? requireNative('tty').isatty(0) || process.env.FORCE_COLOR : false;\n}\ncatch (error) {\n if (error.message.includes('no native require'))\n exports.useChalk = false;\n else\n throw error;\n}\n\n/** \n * Factory function which constructs an error message relating to version mismatches\n * @param {string} oldThingLabel The name of the thing that is too old\n * @param {string} upgradeThingLabel [optional] The name of the thing that needs to be upgraded to fix that; if\n * unspecified, we use oldThingLabel.\n * @param {string} newThingLabel What the thing is that is that is newer than oldThingLabel\n * @param {string} minimumVersion The minimum version of the old thing that the new thing supports. \n * Obstensibibly semver, but unparsed.\n * @param {string} code [optional] A code property to add to the return value\n *\n * @returns instance of Error\n */\nexports.versionError = function dcpClient$$versionError(oldThingLabel, upgradeThingLabel, newThingLabel, minimumVersion, code)\n{\n function repeat(what, len) {\n let out = '';\n while (len--)\n out += what;\n return out;\n }\n\n function bold(string) {\n if ((__webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\").platform) === 'nodejs') {\n const chalk = new requireNative('chalk').constructor({enabled: exports.useChalk});\n\n return chalk.yellow(string);\n }\n\n return string;\n }\n \n var mainMessage = `${oldThingLabel} is too old; ${newThingLabel} needs version ${minimumVersion}`;\n var upgradeMessage = bold(`Please upgrade ${upgradeThingLabel || oldThingLabel}${repeat(\" \", mainMessage.length - 15 - upgradeThingLabel.length)}`);\n var error = new Error(`\n****${repeat('*', mainMessage.length)}****\n*** ${repeat(' ', mainMessage.length)} ***\n*** ${mainMessage} ***\n*** ${upgradeMessage} ***\n*** ${repeat(' ', mainMessage.length)} ***\n****${repeat('*', mainMessage.length)}****\n`);\n\n if (code)\n error.code = code;\n return error;\n}\n\n/**\n * Perf Timers:\n * shortTime\n * class PerfTimers\n *********************************************************************************************/\n\n/**\n * Previous short form perf time stamp.\n * @private\n * @type {Date}\n */\nlet previousTime = new Date();\n\n/**\n * Short form perf time format with timespan diff from last call.\n * @param {boolean} [displayDiffOnly=true]\n * @returns {string}\n */\nexports.shortTime = function util$$shortTime(displayDiffOnly=true) {\n const currentTime = new Date();\n const diff = currentTime.getTime() - previousTime.getTime();\n previousTime = currentTime;\n if (displayDiffOnly) return `diff=${diff}`;\n return `diff=${diff}: ${currentTime.getMinutes()}.${currentTime.getSeconds()}.${currentTime.getMilliseconds()}`;\n}\n\n/**\n * By default returns\n * functionName:lineNumber: message\n * When (!message || !message.length), returns\n * functionName:lineNumber\n *\n * When shortForm is true, returns\n * lineNumber: message\n *\n * @param {string} [message]\n * @param {number} [depth=2]\n * @param {boolean} [shortForm=false]\n * @returns {string}\n */\nexports.debugLine = function debugLine(message, depth = 2, shortForm = false) {\n const e = new Error();\n const frame = e.stack.split(\"\\n\")[depth];\n const lineNumber = frame.split(\":\").reverse()[1];\n const funcName = frame.split(\" \")[5];\n const header = shortForm ? 'line#' : funcName;\n const headerAndLine = `${header}:${lineNumber}`;\n if (!message || !message.length) return headerAndLine;\n return headerAndLine + \" \" + message;\n}\n\n/**\n * class Tracer: Method mark, displays time span since the last mark.\n *\n * When perf testing the first step is to place a bunch of timer entries\n * in the code where the time difference from the last entry is displayed\n * This class Tracer is used as follows:\n * const tracer = new Tracer(tag);\n * ...............................\n * tracer.mark(); // displays dt1: lineNumber: tag -- where dt1 is time since constructor was called\n * ...............................\n * tracer.mark(); // displays dt2: lineNumber: tag -- where dt2 is time since previous mark\n * ...............................\n * tracer.mark(); // displays dt3: lineNumber: tag -- where dt3 is time since previous mark\n *\n * There are many variations possible as can be seen by the comment on the mark method.\n */\nclass Tracer {\n /**\n * \n * @param {boolean} [shortForm=true]\n * @param {string} [tag]\n */\n constructor(shortForm = true, tag) {\n this.shortForm = shortForm;\n this.tag = tag;\n this.previousTime = new Date();\n }\n /**\n * Let diff be the time span since the last call to mark or the Tracer constructor.\n * Let lineNumber be the line # of the file containing the call to mark.\n * Let line# be the string \"line#\"\n * Let funcName be the name of the function where mark was called.\n * When tag.length > 0,\n * displays: diff: line#:lineNumber: tag:message\n * or: diff: line#:lineNumber: tag when !message\n * or: diff: funcName:lineNumber tag:message when !shortForm\n * or: diff: funcName:lineNumber tag when !shortForm and !message\n * else\n * displays: diff: line#:lineNumber: message\n * or: diff: line#lineNumber: when !message\n * or: diff: funcName:lineNumber message when !shortForm\n * or: diff: funcName:lineNumber when !shortForm and !message\n * @param {string} [message]\n */\n mark(message) {\n const currentTime = new Date();\n const diff = currentTime.getTime() - this.previousTime.getTime();\n this.previousTime = currentTime;\n let tag = this.tag;\n if (tag && tag.length && message) tag = `${tag}:${message}`;\n else if (message) tag = message;\n console.log(`${diff}: ${exports.debugLine(tag, 3 /* depth*/, this.shortForm)}`);\n }\n}\nexports.Tracer = Tracer;\n\n/**\n* class PerfTimer: Construct a set of possibly overlapping timers.\n*\n* Example:\n* const perfTimers = new PerfTimers(['timer0', 'timer1', 'timer2']);\n* perfTimers.start('timer0');\n* perfTimers.start('timer1');\n* .........................\n* perfTimers.stop('timer1');\n* .........................\n* perfTimers.stop('timer0');\n* .........................\n* perfTimers.start('timer2');\n* ..........................\n* perfTimers.stop('timer2');\n* perfTimers.sum(name => name !== 'timer1');\n*\n* Should display something like:\n* timer1: 472ms\n* timer0: 1650ms\n* timer2: 2333ms\n* The sum of timers [timer0, timer2] is 3983ms\n*/\nclass PerfTimers {\n /**\n * @constructor\n * @param {string[]} perfTimerNames\n * @param {boolean} [disable=false]]\n * @param {boolean} [summaryView=false]]\n */\n constructor (perfTimerNames, disable = false, summaryView = false) {\n this.disable = disable;\n if (this.disable) return;\n assert(Array.isArray(perfTimerNames));\n /** @type {string[]} */\n this.perfTimerNames = perfTimerNames;\n /** @type {boolean} */\n this.summaryView = summaryView;\n /** @type {number[]} */\n this.perfTimers = Array.from({ length: perfTimerNames.length }, (v, i) => 0);\n /** @type {object} */\n this.perfTimerMap = {};\n for (let k = 0; k < this.perfTimerNames.length; k++)\n this.perfTimerMap[this.perfTimerNames[k]] = k;\n }\n /**\n * Start a timer on perfTimerName\n * @param {string} perfTimerName\n */\n start(perfTimerName) {\n if (this.disable) return;\n const slot = this.perfTimerMap[perfTimerName];\n if (slot === undefined) throw new Error(`PerfDebugging: perfTimer '${perfTimerName}' not found.`);\n this.perfTimers[slot] = Date.now();\n }\n /**\n * Stop a timer on perfTimerName and display the result.\n * @param {string} perfTimerName\n */\n stop(perfTimerName) {\n if (this.disable) return;\n const slot = this.perfTimerMap[perfTimerName];\n if (slot === undefined) throw new Error(`PerfDebugging: perfTimer '${perfTimerName}' not found.`);\n this.perfTimers[slot] = Date.now() - this.perfTimers[slot];\n if (!this.summaryView) console.log(`${perfTimerName}: ${this.perfTimers[slot]}ms`);\n }\n /** \n * @callback PredicateCallback\n * @param {string} value\n * @param {number} index\n * @param {string[]} array\n */\n /**\n * Use predicate to choose a subset of this.perfTimerNames,\n * sum up the corresponding timers and display the result.\n * @param {PredicateCallback} predicate\n * @param {boolean} [forceDisplay=false]\n */\n sum(predicate, forceDisplay = false) {\n if (this.disable || this.summaryView && !forceDisplay) return;\n const names = this.perfTimerNames.filter(predicate);\n const timers = this.perfTimers.map(k => this.perfTimerMap(names[k]));\n const sum = timers.reduce((previous, current) => previous + current, 0);\n console.log(`The sum of timers ${JSON.stringify(names)} is ${sum}ms`);\n }\n /**\n * Display summary.\n * @param {PredicateCallback} predicate\n * @param {boolean} [forceDisplay=false]\n */\n summary(predicate, forceDisplay = false) {\n if (this.disable) return;\n if (this.summaryView || forceDisplay) {\n for (let k = 0; k < this.perfTimers.length; k++)\n console.log(`${this.perfTimerNames[k]}: ${this.perfTimers[k]}ms`);\n }\n if (predicate) this.sum(predicate);\n }\n}\nexports.PerfTimers = PerfTimers;\n\n/**\n * Sandbox and Slice debugging and logging tools.\n * dumpSlices -- dump array of slices\n * dumpSandboxes -- dump array of sandboxes\n * dumpSlicesIfNotUnique -- dump array of slices when there are dups\n * dumpSandboxesIfNotUnique -- dump array of sandboxes when there are dups\n * isUniqueSlices -- detect dups in an array of slices\n * isUniqueSandboxes -- detect dups in an array of sandboxes\n *********************************************************************************************/\n\n/**\n * Log sliceArray.\n * @param {Slice[]} sliceArray\n * @param {string} [header]\n */\nexports.dumpSlices = function utils$$dumpSlices(sliceArray, header) {\n if (header) console.log(`\\n${header}`);\n console.log(exports.compressSlices(sliceArray));\n}\n\n/**\n * Log sandboxArray.\n * @param {Sandbox[]} sandboxArray\n * @param {string} [header]\n */\nexports.dumpSandboxes = function utils$$dumpSandboxes(sandboxArray, header) {\n if (header) console.log(`\\n${header}`);\n console.log(exports.compressSandboxes(sandboxArray));\n}\n\n/**\n * If the elements of sliceArray are not unique, log the duplicates and log the full array.\n * @param {Slice[]} sliceArray\n * @param {string} [header]\n */\nexports.dumpSlicesIfNotUnique = function utils$$dumpSlicesIfNotUnique(sliceArray, header) {\n if (!exports.isUniqueSlices(sliceArray, header))\n console.log(exports.compressSlices(sliceArray));\n}\n\n/**\n * If the elements of sandboxArray are not unique, log the duplicates and log the full array.\n * @param {Sandbox[]} sandboxArray\n * @param {string} [header]\n */\nexports.dumpSandboxesIfNotUnique = function utils$$dumpSandboxesIfNotUnique(sandboxArray, header) {\n if (!exports.isUniqueSandboxes(sandboxArray, header))\n console.log(exports.compressSandboxes(sandboxArray));\n}\n\n/**\n * Checks whether the elements of sliceArray are unique and if not, log the duplicates.\n * @param {Slice[]} sliceArray\n * @param {string} [header]\n * @param {function} [log]\n * @returns {boolean}\n */\nexports.isUniqueSlices = function utils$$isUniqueSlices(sliceArray, header, log) {\n const slices = [];\n let once = true;\n sliceArray.forEach(x => {\n if (slices.indexOf(x) >= 0) {\n if (once && header) console.log(`\\n${header}`); once = false;\n log ? log(x) : console.log(`\\tFound duplicate slice ${x.identifier}.`);\n } else slices.push(x);\n });\n return sliceArray.length === slices.length;\n}\n\n/**\n * Checks whether the elements of sandboxArray are unique and if not, log the duplicates.\n * @param {Sandbox[]} sandboxArray\n * @param {string} [header]\n * @param {function} [log]\n * @returns {boolean}\n */\nexports.isUniqueSandboxes = function utils$$isUniqueSandboxes(sandboxArray, header, log) {\n const sandboxes = [];\n let once = true;\n sandboxArray.forEach(x => {\n if (sandboxes.indexOf(x) >= 0) {\n if (once && header) console.log(`\\n${header}`); once = false;\n log ? log(x) : console.log(`\\tFound duplicate sandbox ${x.identifier}.`);\n } else sandboxes.push(x);\n });\n return sandboxArray.length === sandboxes.length;\n}\n\n/**\n * JSON Serialization\n * stringify -- ignore cycles\n * dumpJSON -- safely wrapped stringify with header\n * dumpObject -- Apply dumpJSON to every [key, value] of Object.entries(theObject)\n *********************************************************************************************/\n\n/**\n * Quck and dirty JSON serialization that ignores cycles.\n *\n * @param {*} theAnything - entity to be serialized.\n * @param {number} [truncationLength=512] - number of string elements to return.\n * @param {number} [space=0] - # of spaces to indent (0 <= space <= 10).\n * @returns {string}\n */\nexports.stringify = function _stringify(theAnything, truncationLength = 512, space = 0) {\n let cache = [];\n const str = JSON.stringify(theAnything, (key, value) => {\n if (typeof value === 'object' && value !== null) {\n if (cache.includes(value)) return;\n cache.push(value);\n }\n return value;\n }, space);\n cache = null;\n return str ? str.slice(0, truncationLength) : null;\n}\n\n/**\n * Calls truncated JSON.stringify on theAnything.\n * @param {*} theAnything\n * @param {string} [header='dumpJSON']\n * @param {number} [truncationLength=512]\n */\nexports.dumpJSON = function utils$$dumpJSON(theAnything, header = 'dumpJSON: ', truncationLength = 512) {\n if (theAnything) {\n const strV = exports.stringify(theAnything, truncationLength);\n if (strV) console.log(`${header}: ${String(strV)}`);\n else console.log(`${header}:`, theAnything);\n } else console.log(`${header}:`, theAnything);\n}\n\n/**\n * Iterates over all property [key, value]-pairs of theObject and call truncated JSON.stringify on property values.\n * @param {object} theObject\n * @param {string} [header='dumpObject']\n * @param {number} [truncationLength=512]\n */\nexports.dumpObject = function utils$$dumpObject(theObject, header = 'dumpObject: ', truncationLength = 512, dumpKeys = true) {\n if (theObject) {\n if (dumpKeys) console.log(`${header}: dump the keys`, Object.keys(theObject));\n console.log(`${header}: dump the key-value entries...`);\n console.group();\n for (const [key, value] of Object.entries(theObject))\n exports.dumpJSON(value, `${header}.${key}`, truncationLength);\n console.groupEnd();\n }\n}\n\n/**\n * Compressed Representations and Maps\n * toJobMap\n * compressSandboxes\n * compressSlices\n * compressJobMap\n * compressJobArray\n * compressJobValue\n * compressJobEntry\n * compressEnhancedJobEntry\n * truncateAddress\n * compressRange\n * compressEnhancedRange\n *********************************************************************************************/\n\n/**\n * @param {object[]} jobArray\n * @param {function} functor\n * @returns {object}\n */\nexports.toJobMap = function utils$$toJobMap(jobArray, functor) {\n const jobMap = {};\n for (const x of jobArray) {\n if (!jobMap[x.jobAddress]) jobMap[x.jobAddress] = [functor(x)];\n else jobMap[x.jobAddress].push(functor(x));\n }\n return jobMap;\n}\n\n/**\n * @param {Sandbox[]} sandboxArray\n * @param {number} [digits=-1]\n * @returns {string}\n */\nexports.compressSandboxes = function utils$$compressSandboxes(sandboxArray, digits = -1) {\n const jobSandboxMap = exports.toJobMap(sandboxArray, sbx => sbx.id);\n return exports.compressJobMap(jobSandboxMap, false /* skipFirst*/, digits);\n}\n\n/**\n * @param {Slice[]} sliceArray\n * @param {number} [digits=-1]\n * @returns {string}\n */\nexports.compressSlices = function utils$$compressSlices(sliceArray, digits = -1) {\n const jobSliceMap = exports.toJobMap(sliceArray, slice => slice.sliceNumber);\n return exports.compressJobMap(jobSliceMap, false /* skipFirst*/, digits);\n}\n\n/**\n * @param {object} jobMap\n * @param {boolean} [skipFirst=false]\n * @param {number} [digits=-1]\n * @returns {string}\n */\nexports.compressJobMap = function utils$$compressJobMap(jobMap, skipFirst = false, digits = -1) {\n return exports.compressJobArray(Object.entries(jobMap), skipFirst, digits);\n}\n\n/**\n * @param {object[]} jobArray\n * @param {boolean} [skipFirst=false]\n * @param {number} [digits=-1]\n * @returns {string}\n */\nexports.compressJobArray = function utils$$compressJobArray(jobArray, skipFirst = false, digits = -1) {\n let output = '';\n for (let k = 0; k < jobArray.length; k++) {\n output += exports.compressJobValue(jobArray[k], skipFirst, digits);\n }\n return output;\n}\n\n/**\n * @param {object|object[]} jobValue\n * @param {boolean} [skipFirst=false]\n * @param {number} [digits=-1]\n * @returns {string}\n */\nexports.compressJobValue = function utils$$compressJobValue(jobValue, skipFirst = false, digits = -1) {\n if (jobValue.job && jobValue.slices)\n return exports.compressJobEntry(jobValue.job, (skipFirst ? jobValue.slices.slice(1) : jobValue.slices), digits);\n if (jobValue.length === 2)\n return exports.compressJobEntry(jobValue[0], (skipFirst ? jobValue[1].slice(1) : jobValue[1]), digits);\n return 'nada';\n}\n\n/**\n * @param {*} jobAddress\n * @param {Array<number|Number>} sliceNumbers\n * @param {number} [digits=-1]\n * @returns {string}\n */\nexports.compressJobEntry = function utils$$compressJobEntry(jobAddress, sliceNumbers, digits = -1) {\n return `${exports.truncateAddress(jobAddress, digits)}:[${exports.compressRange(sliceNumbers)}]:`;\n}\n\nexports.compressEnhancedJobEntry = function utils$$compressEnhancedJobEntry(job, slices, digits = -1) {\n return `${job.id}.${exports.truncateAddress(job.address, digits)}:[${exports.compressEnhancedRange(slices)}]:`;\n}\n\n/**\n * @param {*} address\n * @param {number} [digits=-1]\n * @returns {string}\n */\nexports.truncateAddress = function utils$$truncateAddress(address, digits = -1) {\n let value = address.toString();\n if (digits < 0) return value.startsWith('0x') ? value.substring(2) : fragment;\n return value.startsWith('0x') ? value.substring(2, 2 + digits) : value.substring(0, digits);\n}\n\n/**\n * Input [2, 3, 4, 7, 5, 8, 9, 13, 14, 15] returns '2-5,7-9,13-15'\n * Input [2, 3, 4, 7, 5, 8, 9, 13] returns '2-5,7-9,13'\n * Input [2, 3, 4, 7, 5, 4, 4, 8, 9] returns '2-4,4,4-5,7-9'\n * @param {Array<number|Number>} numberArray\n * @returns {string}\n */\nexports.compressRange = function utils$$compressRange(numberArray) {\n assert(numberArray && Array.isArray(numberArray));\n numberArray.sort((x, y) => x - y); // increasing...\n let start = numberArray[0];\n let output = `${start}`;\n for (let k = 1; k < numberArray.length; k++) {\n assert(typeof numberArray[k] === 'number' || numberArray[k] && numberArray[k].constructor.name === 'Number');\n if (numberArray[k] - numberArray[k - 1] !== 1) {\n output += (numberArray[k - 1] > start) ? `-${numberArray[k - 1]},` : ',';\n start = numberArray[k];\n output += `${start}`;\n } else if (k === numberArray.length - 1) {\n output += `-${numberArray[k]}`;\n }\n }\n return output;\n}\n\nexports.compressEnhancedRange = function utils$$compressEnhancedRange(slices) {\n assert(slices && Array.isArray(slices));\n slices.sort((x, y) => x.sliceNumber - y.sliceNumber); // increasing...\n let start = slices[0];\n let output = fragment(start);\n for (let k = 1; k < slices.length; k++) {\n if (slices[k].sliceNumber - slices[k - 1].sliceNumber !== 1) {\n output += (slices[k - 1].sliceNumber > start.sliceNumber) ? `-${fragment(slices[k - 1])},` : ',';\n start = slices[k];\n output += `${fragment(start)}`;\n } else if (k === slices.length - 1) {\n output += `-${fragment(slices[k])}`;\n }\n }\n return output;\n}\n\nfunction fragment(slice) {\n return `${slice.sliceNumber}.${slice.isEstimationSlice}.${slice.isLongSlice}`;\n}\n\n/**\n * shuffle\n * hashGeneration\n *********************************************************************************************/\n\nexports.shuffle = function utils$$shuffle(jobDescriptors, partitionPortions) {\n const schedulerConstants = __webpack_require__(/*! dcp/common/scheduler-constants */ \"./src/common/scheduler-constants.js\");\n \n let jobDescriptorsInEstimation = [];\n let partitionPortionsInEstimation = [];\n for (let [index, jobDescriptor] of jobDescriptors.entries()) {\n if(jobDescriptor.job.status === schedulerConstants.jobStatus.estimation) {\n jobDescriptorsInEstimation.push(jobDescriptor);\n jobDescriptors.splice(index, 1);\n partitionPortionsInEstimation.push(partitionPortions[index]);\n partitionPortions.splice(index, 1) \n }\n }\n \n let currentIndex = jobDescriptors.length, randomIndex;\n // While there remain elements to shuffle...\n while (0 !== currentIndex && jobDescriptors.length > 0) {\n\n // Pick a remaining element...\n randomIndex = Math.floor(Math.random() * currentIndex);\n currentIndex--;\n\n // And swap it with the current element.\n [jobDescriptors[currentIndex], jobDescriptors[randomIndex]] = [\n jobDescriptors[randomIndex], jobDescriptors[currentIndex]];\n\n [partitionPortions[currentIndex], partitionPortions[randomIndex]] = [\n partitionPortions[randomIndex], partitionPortions[currentIndex]];\n }\n \n return [\n [...jobDescriptorsInEstimation, ...jobDescriptors],\n [...partitionPortionsInEstimation, ...partitionPortions]\n ]\n}\n\nexports.hashGeneration = function utils$$hashGeneration(object) {\n const { sha256 } = (__webpack_require__(/*! dcp/dcp-client/wallet/keystore */ \"./src/dcp-client/wallet/keystore.js\")._internalEth.util);\n return sha256(Buffer.from(JSON.stringify(object)));\n}\n\n/**\n * retrieveSlicesFromJob return type used in Supervisor.\n *********************************************************************************************/\n\n/**\n * @typedef {object} SliceMessage\n * @property {string} jobAddress\n * @property {string} uuid\n * @property {string} worker\n * @property {number} sliceNumber\n * @property {boolean} isEstimationSlice\n * @property {boolean} isLongSlice\n * @property {number} timestamp\n * @property {string} resultStorageType\n * @property {string} resultStorageDetails\n * @property {object} resultStorageParams\n * @property {string} datumUri\n */\n\n/**\n * CG service error handling types and helpers.\n *********************************************************************************************/\n\n/**\n * @typedef {object} apiClientType\n * @property {boolean} success\n * @property {*} [payload]\n * @property {DCPError} [error]\n */\n\n/**\n * @typedef {object} apiServiceType\n * @property {boolean} success\n * @property {*} [payload]\n * @property {string} [message]\n * @property {string} [stack]\n * @property {string} [code]\n */\n\n/**\n * @param {apiServiceType} payload \n * @returns {apiClientType}\n */\nexports.reconstructServiceError = function utils$$reconstructServiceError(payload) {\n assert(!payload.success);\n let ex = new DCPError(payload.message);\n ex.stack = dcpConfig.worker.allowConsoleAccess ? payload.stack : '';\n if (payload.code) ex.code = payload.code;\n return exports._clientError(ex);\n}\n\n/**\n * @param {DCPError} ex\n * @returns {apiClientType}\n */\nexports._clientError = function utils$$_clientError(ex) {\n if (dcpConfig.worker.allowConsoleAccess) console.error(ex);\n return { success: false, error: ex };\n}\n/**\n * @param {string} message\n * @returns {apiClientType}\n */\nexports.clientError = function utils$$clientError(message) {\n const ex = new DCPError(message);\n return exports._clientError(ex);\n}\n\n/**\n * @param {DCPError} ex\n * @returns {apiServiceType}\n */\nexports._serviceError = function utils$$_serviceError(ex) {\n return { success: false, message: ex.message, stack: ex.stack, code: ex.code };\n}\n/**\n * @param {string} message\n * @param {string|object} codeEx\n * @returns {apiServiceType}\n */\nexports.serviceError = function utils$$serviceError(message, codeEx) {\n const ex = new DCPError(message, codeEx);\n return exports._serviceError(ex);\n}\n\n/**\n * exports\n *********************************************************************************************/\n\nif ((__webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\").platform) === 'nodejs') {\n Object.assign(exports, __webpack_require__(/*! ./tmpfiles */ \"./src/utils/tmpfiles.js\"));\n Object.assign(exports, __webpack_require__(/*! ./readln */ \"./src/utils/readln.js\"));\n}\nmodule.exports.encodeDataURI = __webpack_require__(/*! ./encodeDataURI */ \"./src/utils/encodeDataURI.js\").encodeDataURI;\n\nObject.assign(exports, __webpack_require__(/*! ./sh */ \"./src/utils/sh.js\"));\nObject.assign(exports, __webpack_require__(/*! ./eventUtils */ \"./src/utils/eventUtils.js\"));\nObject.assign(exports, __webpack_require__(/*! ./obj-merge */ \"./src/utils/obj-merge.js\"));\nObject.assign(exports, __webpack_require__(/*! ./make-data-uri */ \"./src/utils/make-data-uri.js\"));\nObject.assign(exports, __webpack_require__(/*! ./just-fetch */ \"./src/utils/just-fetch.js\"));\nObject.assign(exports, __webpack_require__(/*! ./inventory */ \"./src/utils/inventory.js\"));\nObject.assign(exports, __webpack_require__(/*! ./fetch-keystore */ \"./src/utils/fetch-keystore.js\"));\nObject.assign(exports, __webpack_require__(/*! ./fetch-uri */ \"./src/utils/fetch-uri.js\"));\n\n\n//# sourceURL=webpack://dcp/./src/utils/index.js?");
4803
4944
 
4804
4945
  /***/ }),
4805
4946
 
@@ -4821,7 +4962,7 @@ eval("/**\n * @file src/utils/inventory.js\n * Inventory met
4821
4962
  /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
4822
4963
 
4823
4964
  "use strict";
4824
- eval("/* provided dependency */ var process = __webpack_require__(/*! ./node_modules/process/browser.js */ \"./node_modules/process/browser.js\");\n/**\n * @file just-fetch.js\n * @author Ryan Rossiter <ryan@kingsds.network>\n * @date June 2020\n *\n * Cross-platform method for performing an HTTP request.\n */\n\n\nconst { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\nconst { DcpURL } = __webpack_require__(/*! dcp/common/dcp-url */ \"./src/common/dcp-url.js\");\nconst kvin = __webpack_require__(/*! kvin */ \"./node_modules/kvin/kvin.js\");\n\n /**\n * Make a request, as a promise, resolves with a string or json object\n *\n * @param {string | object} url - path of file to be fetched; if object must be instanceof URL or DcpURL\n * @param {string} [type = \"string\"] - expected type of the response\n * @param {string} [method = \"GET\"] - type of request to make\n * @param {boolean} [bustCache = false] - whether to add cache poison to the end of the url\n * @param {any} [body] - what to send as the body of the message. Objects are turned into into \n * standard CGI key=value encoding when the method is 'POST'\n * @returns {Promise} - resolves with response (json if type=\"JSON\", string otherwise), rejects on fail\n */\nexports.justFetch = function justFetch(url, type = 'string', method = 'GET', bustCache = false, body = undefined) {\n return new Promise((resolve, reject) => {\n let deeperErrorStack = new Error().stack;\n deeperErrorStack = deeperErrorStack.substring(deeperErrorStack.indexOf('\\n') + 1);\n\n if (bustCache)\n {\n let bustParam = ((typeof document !== 'undefined' && document.head && document.head.getAttribute('version')) || Date.now()); /* cache buster */\n\n if (typeof url === 'object' && url instanceof DcpURL)\n url = url.href;\n if (typeof url === 'string')\n url = new URL(url);\n url.search += (url.search ? '&' : '') + encodeURI(bustParam);\n }\n\n if (typeof url === 'object' && url.href) {\n url = url.href;\n }\n\n const xhr = new XMLHttpRequest();\n xhr.onloadend = function Protocol$$justFetch$onloadend() {\n try {\n delete xhr.onloadend;\n\n if (xhr.status >= 200 && xhr.status < 300) {\n if (xhr.getResponseHeader('content-type') && type === 'string') {\n type = xhr.getResponseHeader('content-type').split(';')[0];\n }\n \n let data = xhr.responseText;\n if (type === 'JSON' || type === 'application/json') {\n data = JSON.parse(data);\n }\n\n if (type === 'application/x-kvin' ) {\n data = kvin.deserialize(data);\n }\n \n resolve(data);\n } else {\n function makeFetchError()\n {\n if (xhr.status)\n return new Error(`HTTP Error ${xhr.status} fetching ${url}`);\n\n /* NodeJS via xmlhttprequest-ssl leaves an Error duck */\n if (xhr.statusText && xhr.statusText.code && xhr.statusText.message) {\n let error = new Error(xhr.statusText.message);\n let stack = error.stack;\n\n Object.assign(error, xhr.statusText);\n if (xhr.statusText.stack)\n error.stack = xhr.statusText.stack.split('\\n').slice(1).join('\\n') + '---\\n' + stack;\n else\n error.stack = stack;\n return error;\n }\n\n return new Error(`Network error fetching ${url}`);\n }\n\n const error = makeFetchError();\n \n error.request = Object.assign({}, xhr); /* Shallow clone so that console.log will work in node */\n error.request.method = method;\n error.request.location = url;\n error.stack += '\\n' + deeperErrorStack;\n if (xhr.status)\n error.code = 'HTTP_' + xhr.status;\n\n throw error;\n }\n } catch (e) {\n reject(e);\n }\n };\n\n xhr.open(method, url);\n if (!body)\n xhr.send();\n else\n {\n if (typeof body === 'object' && method.toUpperCase() === 'POST')\n {\n let entries = Object.entries(body);\n body = entries.map((kvp) => `${encodeURIComponent(kvp[0])}=${encodeURIComponent(kvp[1])}`).join('&');\n }\n xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');\n xhr.send(body);\n }\n });\n }\n\n/** Reformat an error (rejection) message from utils::justFetch, so that debugging code \n * can include (for example) a text-rendered version of the remote 404 page.\n *\n * @param {object} error The rejection from justFetch()\n * @returns {string} An error message, formatted with ANSI color when the output\n * is a terminal, suitable for writing directly to stdout. If\n * the response included html content (eg a 404 page), it is \n * rendered to text in this string.\n */\nexports.justFetchPrettyError = function utils$$justFetchPrettyError(error) {\n const chalk = new requireNative('chalk').constructor({enabled: (__webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\").useChalk)});\n var message, headers={};\n\n if (!error.request || !error.request.status)\n return error;\n\n error.request.getAllResponseHeaders().replace(/\\r/g,'').split('\\n').forEach(function(line) {\n var colon = line.indexOf(': ')\n headers[line.slice(0,colon)] = line.slice(colon+2)\n })\n\n message = `HTTP Status: ${error.request.status} for ${error.request.method} ${error.request.location}`\n\n switch(headers['content-type'].replace(/;.*$/, '')) {\n case 'text/plain':\n message += '\\n' + chalk.grey(error.request.responseText)\n break;\n case 'text/html': {\n let html = error.request.responseText;\n\n html = html.replace(/\\n<a/gi, ' <a'); /* html-to-text bug, affects google 301s /wg jun 2020 */\n message += chalk.grey((__webpack_require__(/*! html-to-text */ \"./node_modules/html-to-text/index.js\").htmlToText)(html, {\n wordwrap: parseInt(process.env.COLUMNS, 10) || 80,\n hideLinkHrefIfSameAsText: true,\n format: {\n heading: function (elem, fn, options) {\n var h = fn(elem.children, options);\n return '\\n====\\n' + chalk.yellow(chalk.bold(h.toUpperCase())) + '\\n====\\n';\n }\n }\n }));\n break;\n }\n }\n\n return message;\n} \n\n\n//# sourceURL=webpack://dcp/./src/utils/just-fetch.js?");
4965
+ eval("/* provided dependency */ var process = __webpack_require__(/*! ./node_modules/process/browser.js */ \"./node_modules/process/browser.js\");\n/**\n * @file just-fetch.js\n * @author Ryan Rossiter <ryan@kingsds.network>\n * @date June 2020\n *\n * Cross-platform method for performing an HTTP request.\n */\n\n\nconst { assert } = __webpack_require__(/*! dcp/common/dcp-assert */ \"./src/common/dcp-assert.js\");\nconst { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\nconst { DcpURL } = __webpack_require__(/*! dcp/common/dcp-url */ \"./src/common/dcp-url.js\");\nconst kvin = __webpack_require__(/*! kvin */ \"./node_modules/kvin/kvin.js\");\n\n /**\n * Make a request, as a promise, resolves with a string or json object\n *\n * @param {string | object} url - path of file to be fetched; if object must be instanceof URL or DcpURL\n * @param {string} [type = \"string\"] - expected type of the response\n * @param {string} [method = \"GET\"] - type of request to make\n * @param {boolean} [bustCache = false] - whether to add cache poison to the end of the url\n * @param {any} [body] - what to send as the body of the message. Objects are turned into into \n * standard CGI key=value encoding when the method is 'POST'\n * @returns {Promise} - resolves with response (json if type=\"JSON\", string otherwise), rejects on fail\n */\nexports.justFetch = function justFetch(url, type = 'string', method = 'GET', bustCache = false, body = undefined) {\n return new Promise((resolve, reject) => {\n let deeperErrorStack = new Error().stack;\n deeperErrorStack = deeperErrorStack.substring(deeperErrorStack.indexOf('\\n') + 1);\n \n if (typeof url === 'object' && typeof url.href !== 'undefined')\n {\n assert(url.protocol !== 'data:');\n url = url.href;\n }\n\n if (bustCache)\n {\n let bustParam = ((typeof document !== 'undefined' && document.head && document.head.getAttribute('version')) || Date.now()); /* cache buster */\n\n url = new URL(url);\n url.search += (url.search ? '&' : '') + encodeURI(bustParam);\n url = url.href;\n }\n /* url now guaranteed to be a string */\n \n const xhr = new XMLHttpRequest();\n xhr.onloadend = function Protocol$$justFetch$onloadend() {\n try {\n delete xhr.onloadend;\n\n if (xhr.status >= 200 && xhr.status < 300) {\n if (xhr.getResponseHeader('content-type') && type === 'string') {\n type = xhr.getResponseHeader('content-type').split(';')[0];\n }\n \n let data = xhr.responseText;\n if (type === 'JSON' || type === 'application/json') {\n data = JSON.parse(data);\n }\n\n if (type === 'application/x-kvin' ) {\n data = kvin.deserialize(data);\n }\n \n resolve(data);\n } else {\n function makeFetchError()\n {\n if (xhr.status)\n return new Error(`HTTP Error ${xhr.status} fetching ${url}`);\n\n /* NodeJS via xmlhttprequest-ssl leaves an Error duck */\n if (xhr.statusText && xhr.statusText.code && xhr.statusText.message) {\n let error = new Error(xhr.statusText.message);\n let stack = error.stack;\n\n Object.assign(error, xhr.statusText);\n if (xhr.statusText.stack)\n error.stack = xhr.statusText.stack.split('\\n').slice(1).join('\\n') + '---\\n' + stack;\n else\n error.stack = stack;\n return error;\n }\n\n return new Error(`Network error fetching ${url}`);\n }\n\n const error = makeFetchError();\n \n error.request = Object.assign({}, xhr); /* Shallow clone so that console.log will work in node */\n error.request.method = method;\n error.request.location = url;\n error.stack += '\\n' + deeperErrorStack;\n if (xhr.status)\n error.code = 'HTTP_' + xhr.status;\n\n throw error;\n }\n } catch (e) {\n reject(e);\n }\n };\n\n xhr.open(method, url);\n if (!body)\n xhr.send();\n else\n {\n if (typeof body === 'object' && method.toUpperCase() === 'POST')\n {\n let entries = Object.entries(body);\n body = entries.map((kvp) => `${encodeURIComponent(kvp[0])}=${encodeURIComponent(kvp[1])}`).join('&');\n }\n xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');\n xhr.send(body);\n }\n });\n }\n\n/** Reformat an error (rejection) message from utils::justFetch, so that debugging code \n * can include (for example) a text-rendered version of the remote 404 page.\n *\n * @param {object} error The rejection from justFetch()\n * @returns {string} An error message, formatted with ANSI color when the output\n * is a terminal, suitable for writing directly to stdout. If\n * the response included html content (eg a 404 page), it is \n * rendered to text in this string.\n */\nexports.justFetchPrettyError = function utils$$justFetchPrettyError(error) {\n const chalk = new requireNative('chalk').constructor({enabled: (__webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\").useChalk)});\n var message, headers={};\n\n if (!error.request || !error.request.status)\n return error;\n\n error.request.getAllResponseHeaders().replace(/\\r/g,'').split('\\n').forEach(function(line) {\n var colon = line.indexOf(': ')\n headers[line.slice(0,colon)] = line.slice(colon+2)\n })\n\n message = `HTTP Status: ${error.request.status} for ${error.request.method} ${error.request.location}`\n\n switch(headers['content-type'].replace(/;.*$/, '')) {\n case 'text/plain':\n message += '\\n' + chalk.grey(error.request.responseText)\n break;\n case 'text/html': {\n let html = error.request.responseText;\n\n html = html.replace(/\\n<a/gi, ' <a'); /* html-to-text bug, affects google 301s /wg jun 2020 */\n message += chalk.grey((__webpack_require__(/*! html-to-text */ \"./node_modules/html-to-text/index.js\").htmlToText)(html, {\n wordwrap: parseInt(process.env.COLUMNS, 10) || 80,\n hideLinkHrefIfSameAsText: true,\n format: {\n heading: function (elem, fn, options) {\n var h = fn(elem.children, options);\n return '\\n====\\n' + chalk.yellow(chalk.bold(h.toUpperCase())) + '\\n====\\n';\n }\n }\n }));\n break;\n }\n }\n\n return message;\n} \n\n\n//# sourceURL=webpack://dcp/./src/utils/just-fetch.js?");
4825
4966
 
4826
4967
  /***/ }),
4827
4968
 
@@ -4831,7 +4972,7 @@ eval("/* provided dependency */ var process = __webpack_require__(/*! ./node_mod
4831
4972
  \************************************/
4832
4973
  /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
4833
4974
 
4834
- eval("/**\n * @file make-data-uri.js\n * @author Eddie Roosenmaallen, eddie@kingsds.netework\n * @date Oct 2020\n */\n\n/** \n * Make a dataURI, given the storage type and details.\n * @param {string} storageType The storage type (range|pattern)\n * @param {string} storageDetails Details about the storage and/or data, which\n * varies based on storageType.\n * - range → storageDetails is a range descriptor. The 'slice' parameter is used to specify\n * element of the range to create the URL for.\n * - pattern → storageDetails is a pattern to form a URI-encoded URI that can be fetched\n * by the supervisor, created by substituting parameters into the pattern.\n * @param {object} parameters An object used for making substitutions in URIs. Each object key is\n * treated as a label which can appear between curly braces in the pattern;\n * the corresponding value is then substituted into the string. A special\n * key, hash, is used to indicate that the substituted value should be the\n * the result of calculating the hash of all the other values, appended\n * together in alphabetical order of their keys.\n * @returns {string} the slice URI. This could be any URI suitable for the fetchURI function, including\n * http:, https: and data: URIs.\n */\nexports.makeDataURI = function makeDataURI(storageType, storageDetails, parameters) {\n const { encodeDataURI, hashGeneration } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\n const { rehydrateRange } = __webpack_require__(/*! dcp/dcp-client/range-object */ \"./src/dcp-client/range-object.js\");\n\n if (!parameters || !(typeof parameters === 'object')) {\n throw new Error('parameters object is required');\n }\n\n if (!storageDetails) {\n throw new Error('storageDetails is required');\n }\n\n switch (storageType) {\n case 'range': {\n const ro = rehydrateRange(storageDetails);\n if (!parameters.slice) {\n throw new Error('Slice parameter is required for range storageType');\n }\n\n // -1 to prevent an OBOE since slice numbers start at 1.\n return encodeDataURI(ro[parameters.slice - 1]);\n }\n case 'pattern':\n const uri = Object.entries(parameters).reduce(\n (uri, [key, value]) => {\n const re = new RegExp(`{${key}}`, 'g');\n return uri.replace(re, value)\n },\n storageDetails\n );\n \n /* parameter properties are hashed in alphabetic, rather than enumeration order (usually property insert order), to make hash generation deterministic. See DCP-1465, DCP-2584. */\n let newObj = Object.keys(parameters).sort().reduce((obj, key) => {\n obj[key] = parameters[key];\n return obj;\n }, {});\n \n const hash = hashGeneration(newObj).toString('hex');\n return uri.replace(/{hash}/g, hash);\n \n default:\n throw new Error(`Unknown storageType parameter \"${storageType}\"`);\n }\n}\n\n\n//# sourceURL=webpack://dcp/./src/utils/make-data-uri.js?");
4975
+ eval("/* provided dependency */ var Buffer = __webpack_require__(/*! ./node_modules/node-polyfill-webpack-plugin/node_modules/buffer/index.js */ \"./node_modules/node-polyfill-webpack-plugin/node_modules/buffer/index.js\")[\"Buffer\"];\n/**\n * @file make-data-uri.js\n * @author Eddie Roosenmaallen, eddie@kingsds.netework\n * @date Oct 2020\n */\n\nconst { leafMerge } = __webpack_require__(/*! dcp/utils/obj-merge */ \"./src/utils/obj-merge.js\");\nconst { rehydrateRange } = __webpack_require__(/*! dcp/dcp-client/range-object */ \"./src/dcp-client/range-object.js\");\nconst { encodeDataURI, generateOpaqueId } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\nconst { sha256 } = __webpack_require__(/*! ethereumjs-util */ \"./node_modules/ethereumjs-util/dist.browser/index.js\");\n\n/** \n * Make a URI for the given the storage type and details.\n * @param {string} storageType The storage type (range|pattern)\n * @param {string} storageDetails Details about the storage and/or data, which\n * varies based on storageType.\n * - range → storageDetails is a range descriptor. The 'slice' parameter is used to specify\n * element of the range to create the URL for.\n * - pattern → storageDetails is a pattern to form a URI-encoded URI that can be fetched\n * by the supervisor, created by substituting parameters into the pattern.\n * @param {object} parameters An object used for making substitutions in URIs. Each object key is\n * treated as a label which can appear between curly braces in the pattern;\n * the corresponding value is then substituted into the string.\n *\n * A special key, hash, is used to indicate that the substituted value\n * should be the the result of calculating the hash of all the other values,\n * appended together in alphabetical order of their keys.\n *\n * Another special key, unqiue, is used to generate a random string of\n * characters which is probably unique.\n *\n * Both hash and unique are lazily-generated to avoid overhead. This\n * routine guarantees that parameters will not be mutated.\n *\n * @returns {string} the slice URI. This could be any URI suitable for the fetchURI function, including\n * http:, https: and data: URIs.\n */\nexports.makeValueURI = function makeValueURI(storageType, storageDetails, parameters) {\n const uriRegex = /^[a-z]+:[^ ]*/; /* note: debug only, not comprehensive */;\n\n if (!parameters || !(typeof parameters === 'object')) {\n throw new Error('parameters object is required');\n }\n\n if (!storageDetails) {\n throw new Error('storageDetails is required');\n }\n\n switch (storageType) {\n case 'range': {\n const ro = rehydrateRange(storageDetails);\n if (!parameters.slice) {\n throw new Error('Slice parameter is required for range storageType');\n }\n\n // -1 to prevent an OBOE since slice numbers start at 1.\n return encodeDataURI(ro[parameters.slice - 1]);\n }\n case 'pattern':\n const { assert } = __webpack_require__(/*! dcp/common/dcp-assert */ \"./src/common/dcp-assert.js\");\n assert(uriRegex.test(storageDetails)); /* ensure storageDetails is (probably) a URI pattern */\n\n /** @todo XXXwg - {key:1,2} should eventually have the args after the colon become parameters to string.slice */\n const uri = Object.entries(parameters).reduce(\n (uri, [key, value]) => {\n const re = new RegExp(`{${key}}`, 'g');\n return uri.replace(re, value)\n },\n storageDetails\n );\n \n if (uri.indexOf('{hash}') === -1)\n return uri; /* avoid generating hash when not needed */\n const hash = sha256(Buffer.from(JSON.stringify(parameters))).toString('hex');\n return uri.replace(/{hash}/g, hash);\n \n default:\n throw new Error(`Unknown storageType parameter \"${storageType}\"`);\n }\n}\n\n/**\n * Factory function which returns the curated parameters associated with a given given job/slice; these\n * patterns are used for the makeValueURI etc function to expand a URI pattern into a URI. See DCP-1480.\n *\n * @param {object} job The jobs table row corresponding to the job\n * @param {object} extraParams An object with extra parameters (eg sliceNumber) to add to the \n * returned value.\n */\n exports.makePatternParams = function utils$$makePatternParams(job, extraParams)\n {\n return leafMerge({\n job: job.address,\n jobId: job.uuid,\n }, extraParams);\n }\n\n//# sourceURL=webpack://dcp/./src/utils/make-data-uri.js?");
4835
4976
 
4836
4977
  /***/ }),
4837
4978
 
@@ -4851,7 +4992,7 @@ eval("\n// This import will use built-in buffer in node, and the buffer package
4851
4992
  \********************************/
4852
4993
  /***/ ((__unused_webpack_module, exports) => {
4853
4994
 
4854
- eval("/**\n * @file obj-merge.js\n * Routines for merging objects in different ways\n *\n * @author Wes Garland, wes@page.ca\n * @date Dec 2020\n */\n\n/** \n * Merge objects at their leaves, combining intermediary objects as necessary. \n * Arrays are treated as units, not objects. Any number of objects may be specified\n * on the argument vector. The objects on the left are considered to have lower\n * precedence (replaced more easily) than objects on the right. \n *\n * @examples\n * leafMerge({a:1}, {a:2}) => {a:1}\n * leafMerge({a:1}, {b:2}) => {a:1, b:2}\n * leafMerge({a:{b:1}}, {a:{b:2}}) => {a:{b:1}}\n * leafMerge({a:{b:1}}, {b:{c:2}}) => {a:{b:1}}\n * leafMerge({a:{b:1}}, {a:{c:2}}) => {a:{b:1, c:2}}\n *\n * @param [...] Objects to merge\n * @returns new object\n */\nexports.leafMerge = function utils$$objMerge$leafMerge() {\n var target = {};\n \n for (let i=0; i < arguments.length; i++) {\n let neo = arguments[i];\n if (neo === undefined)\n continue;\n \n for (let prop in neo) {\n if (!neo.hasOwnProperty(prop))\n continue;\n if (typeof neo[prop] === 'object' && neo[prop] !== null && !Array.isArray(neo[prop]) && ['Function','Object'].includes(neo[prop].constructor.name)) {\n target[prop] = exports.leafMerge(target[prop], neo[prop]);\n } else {\n target[prop] = neo[prop];\n }\n }\n }\n\n return target;\n}\n\n\n//# sourceURL=webpack://dcp/./src/utils/obj-merge.js?");
4995
+ eval("/**\n * @file obj-merge.js\n * Routines for merging objects in different ways\n *\n * @author Wes Garland, wes@page.ca\n * @date Dec 2020\n */\n\n/** \n * Merge objects at their leaves, combining intermediary objects as necessary. \n * Arrays are treated as units, not objects. Any number of objects may be specified\n * on the argument vector. The objects on the left are considered to have lower\n * precedence (replaced more easily) than objects on the right. \n *\n * @examples\n * leafMerge({a:1}, {a:2}) => {a:2}\n * leafMerge({a:1}, {b:2}) => {a:1, b:2}\n * leafMerge({a:{b:1}}, {a:{b:2}}) => {a:{b:2}}\n * leafMerge({a:{b:1}}, {b:{c:2}}) => {a:{b:{c:2}}}\n * leafMerge({a:{b:1}}, {a:{c:2}}) => {a:{b:1, c:2}}\n *\n * @param [...] Objects to merge\n * @returns new object\n */\nexports.leafMerge = function utils$$objMerge$leafMerge() {\n var target = {};\n \n for (let i=0; i < arguments.length; i++) {\n let neo = arguments[i];\n if (neo === undefined)\n continue;\n \n for (let prop in neo) {\n if (!neo.hasOwnProperty(prop))\n continue;\n if (typeof neo[prop] === 'object' && neo[prop] !== null && !Array.isArray(neo[prop]) && ['Function','Object'].includes(neo[prop].constructor.name)) {\n target[prop] = exports.leafMerge(target[prop], neo[prop]);\n } else {\n target[prop] = neo[prop];\n }\n }\n }\n\n return target;\n}\n\n\n//# sourceURL=webpack://dcp/./src/utils/obj-merge.js?");
4855
4996
 
4856
4997
  /***/ }),
4857
4998
 
@@ -4904,7 +5045,7 @@ eval("/* provided dependency */ var process = __webpack_require__(/*! ./node_mod
4904
5045
  /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
4905
5046
 
4906
5047
  "use strict";
4907
- eval("/* provided dependency */ var Buffer = __webpack_require__(/*! ./node_modules/node-polyfill-webpack-plugin/node_modules/buffer/index.js */ \"./node_modules/node-polyfill-webpack-plugin/node_modules/buffer/index.js\")[\"Buffer\"];\n/** \n * @file tmpfiles.js Utilities for securely and portably creating temporary\n * files with NodeJS.\n * @author Wes Garland, wes@kingsds.network\n * @date April 2020\n */\n\nconst { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\nconst path = requireNative('path');\nconst fs = requireNative('fs');\nconst os = requireNative('os');\nconst process = requireNative('process');\nconst { assert } = __webpack_require__(/*! dcp/common/dcp-assert */ \"./src/common/dcp-assert.js\");\n\nlet systemTempDir = process.env.TMP || process.env.TEMP;\nif (!systemTempDir) {\n if (process.platform === 'win32') {\n if (process.env.LOCALAPPDATA) \n systemTempDir = path.join(systemTempdir, 'Temp');\n else if (process.env.HOMEDRIVE && process.env.HOMEPATH)\n systemTempDir = path.join(process.env.HOMEDRIVE + process.env.HOMEPATH, 'AppData', 'Local', 'Temp');\n else if (process.env.SystemRoot)\n systemTempDir = path.join(systemRoot, 'Temp');\n else if (process.env.SystemDrive)\n systemTempDir = path.join(systemDrive + '\\\\Windows', 'Temp');\n else\n systemTempDir = 'C:\\\\WINDOWS\\\\Temp'\n }\n else\n systemTempDir = '/tmp';\n}\n\nexports.systemTempDir = systemTempDir;\n\n/**\n * Create a temporary thing in the filesystem.\n *\n * @param {function} createThingFn The function which creates the thing; must take as its argument\n * a fully-pathed filename and throw an exception failure. It must\n * return a handle to the thing, which will become the return value\n * of this function.\n * @param {string} basenamePattern A string specifying the file basename pattern. The\n * character X will be replaced with randomly-generated\n * characters, similar to mkstemp(3).\n * @param {string} extension [optional] The filename extension to use; pass false for none\n *\n * @return a handle created by createThingFn\n */\nexports.createTempThing = function createTempThing(createThingFn, basenamePattern, extension)\n{\n const generateEntropy = requireNative('nanoid').customAlphabet('1234567890abcdefghijklmnopqrstuvwyxzkABCDEFGHIJKLMNOPQRSTUVWXYZ', 64);\n\n var entropy = generateEntropy();\n var basename = '';\n var filename; /* The filename of the temp thing we are creating */\n var thing; /* The temp thing we are creating */\n var removed = false; /* Used by removeFn to avoid races */\n var pipeDir;\n \n if (os.platform() === 'win32')\n pipeDir = '\\\\\\\\.\\\\pipe\\\\';\n else\n pipeDir = exports.systemTempDir;\n \n if (!basenamePattern)\n basenamePattern = `dcp-${process.pid}-XXXXXXXX`;\n\n const re = /[xX]{3,}/;\n assert(re.test(basenamePattern))\n\n let tries = 0 \n loop: while (\"goto considered harmful considered harmful\") {\n let j = Math.floor(Math.random() * entropy.length);\n for (let i=0; i < basenamePattern.length; i++) {\n if (basenamePattern[i] === 'X')\n basename += entropy[j++ % entropy.length];\n else\n basename += basenamePattern[i];\n }\n\n if (extension !== false)\n basename += '.' + (extension || 'dcp');\n filename = path.join(pipeDir, basename);\n if (fs.existsSync(filename)) {\n tries = tries++; \n basename='';\n if (tries > 1000)\n throw e\n continue loop;\n }\n try {\n thing = createThingFn(filename);\n } catch(e) {\n if (e.code === 'EEXIST')\n continue;\n throw e;\n }\n break loop;\n }\n\n function removeFn()\n {\n process.off('exit', removeFn);\n if (!process.env.DCP_DEBUG_TMP_FILES && !removed)\n {\n try\n {\n try\n {\n fs.unlinkSync(filename);\n removed = true;\n }\n catch(error)\n {\n if (fs.lstatSync(filename).isDirectory())\n fs.rmdirSync(filename, { recursive: true, maxRetries: 2 });\n else\n throw error;\n }\n }\n catch(error)\n {\n console.warn(`Warning: unable to clean up '${filename}'`, error.message);\n }\n }\n }\n \n if (os.platform() !== 'win32')\n process.on('exit', removeFn); /* Pipe namespace automatically cleaned up on win32 */\n\n return { thing, removeFn, filename };\n}\n\n/** \n * Securely create a temporary file in the system temp directory. A cleanup is registered to\n * automatically remove the file when the process is closed.\n * \n * @param {string} basenamePattern A string specifying the file basename pattern. The\n * character X will be replaced with randomly-generated\n * characters, similar to mkstemp(3).\n * @param {string} extension [optional] The filename extension to use; pass false for none\n * @param perms [optional] The permissions argument to openSync.\n *\n * @returns {object} An object with properties:\n * fd: the open file descriptor\n * filename: the name of the newly-created file\n * toString: method which returns the filename\n * write: method which writes to the file descriptor (like fs.write)\n * writeSync: method which writes to the file descriptor (like fs.writeSync)\n * close: method which closes the file descriptor (like fs.close)\n * closeSync: method which closes the file descriptor (like fs.closeSync)\n * chmod: method which changes the file mode (like fs.fchmod)\n * chmodSync: method which changes the file mode (like fs.fchmodSync)\n * remove: method which remove the file from disk and cleans up the registered\n * process.exit cleanup. Does not close the file descriptor!\n */\nexports.createTempFile = function utils$$createTempFile(basenamePattern, extension, perms) {\n const inspect = requireNative('util').inspect.custom;\n let f = fs.constants;\n let hnd;\n\n /** Atomically create a file using the permissions in the closed-over environment.\n * Atomic here means that either the file will be created or it won't be, and the\n * check-then-create race condition is handled by the OS.\n */\n function atomicCreateFile(filename) {\n return fs.openSync(filename, f.O_RDWR | f.O_CREAT | f.O_EXCL, perms | 0o600);\n }\n\n const { thing: fd, removeFn, filename } = exports.createTempThing(atomicCreateFile, basenamePattern, extension);\n\n hnd = { fd, filename };\n hnd.toString = () => filename;\n hnd.write = function() { return fs.write .apply(null, [fd].concat(Array.from(arguments))) };\n hnd.writeSync = function() { return fs.writeSync .apply(null, [fd].concat(Array.from(arguments))) };\n hnd.chmod = function() { return fs.fchmod .apply(null, [fd].concat(Array.from(arguments))) };\n hnd.chmodSync = function() { return fs.fchmodSync.apply(null, [fd].concat(Array.from(arguments))) };\n hnd.close = function() { fs.close .apply(null, [fd].concat(Array.from(arguments))); delete hnd.fd; return hnd };\n hnd.closeSync = function() { fs.closeSync .apply(null, [fd].concat(Array.from(arguments))); delete hnd.fd; return hnd };\n hnd.remove = removeFn;\n\n hnd[inspect] = () => '[Object utils$$tempFile(' + filename + ')]';\n \n return hnd;\n}\n\n/** \n * Securely create a temporary socket (eg for IPC) in the system temp directory. A cleanup is registered to\n * automatically remove the file when the process is closed.\n * \n * @param {string} basenamePattern A string specifying the file basename pattern. The\n * character X will be replaced with randomly-generated\n * characters, similar to mkstemp(3).\n * @param {string} extension [optional] The filename extension to use; pass false for none\n * @param perms [optional] The permissions argument to openSync. Default is 0600;\n * pass false to use OS default (0666 & umask(2)).\n * @param {function} connectHandler [optional] The callback to invoke when the socket is connected to\n * @returns {object} a socket, created with net.createConnection, using a path argument\n */\nexports.createTempSocket = function utils$$createTempSocket(basenamePattern, extension, connectHandler, perms) {\n const inspect = requireNative('util').inspect.custom;\n\n\n function createSocket(filename)\n {\n const net = requireNative('net');\n var server = net.createServer(connectHandler);\n var socket = server.listen(filename); /* creates the socket (named pipe) */\n\n socket.unref();\n\n return net.createConnection(filename);\n }\n\n const { thing: socket, removeFn, filename } = exports.createTempThing(createSocket, basenamePattern, extension);\n assert(os.platform() !== 'win32' || filename.startsWith('\\\\\\\\.\\\\pipe\\\\'));\n\n if (!socket)\n throw new Error(`Could not create socket ${filename || 'like ' + basenamePattern + '.' + extension}`); /* this should be impossible */\n\n if (filename && perms !== false) {\n try {\n fs.chmodSync(filename, perms || 0o600);\n } catch(error) {\n if (socket.close)\n socket.close();\n console.log(`Could not change permissions on named pipe ${filename} (${error.code || error.message}); closing`);\n throw error;\n }\n }\n\n socket[inspect] = () => '[Object utils$$tempSocket(' + filename + ')]';\n socket.on('close', removeFn);\n return socket;\n}\n\n/**\n * Copy the entire source file into the target, appending at the current\n * file offset. If source file has been closed, we will re-open for the\n * the duration of this call in read-only mode. This routine does not attempt\n * to preserve the file position of the input file.\n *\n * @param source {object} The file to copy, handle from createTempFile\n * @param target {object} The file to append to, handle from createTempFile\n * @param target2 {object} An optioanl stream which also receives the same \n * data via its write method. For npm create-hash.\n */ \nexports.catFile = function utils$$catFile(source, target, target2) {\n var buf = Buffer.allocUnsafe(8192 * 64);\n var pos = 0;\n var nRead;\n var sourceFd = source.fd;\n var targetFd = target.fd;\n\n if (!sourceFd && sourceFd !== 0) {\n sourceFd = fs.openSync(source.filename, fs.O_RDONLY, 0o400);\n }\n \n do {\n nRead = fs.readSync(sourceFd, buf, 0, buf.length, pos);\n fs.writeSync(targetFd, buf.slice(0, nRead));\n pos += nRead;\n if (target2)\n target2.write(buf);\n } while(nRead === buf.length);\n\n if (!source.fd && sourceFd !== 0) {\n fs.closeSync(sourceFd);\n }\n}\n\n\n//# sourceURL=webpack://dcp/./src/utils/tmpfiles.js?");
5048
+ eval("/* provided dependency */ var Buffer = __webpack_require__(/*! ./node_modules/node-polyfill-webpack-plugin/node_modules/buffer/index.js */ \"./node_modules/node-polyfill-webpack-plugin/node_modules/buffer/index.js\")[\"Buffer\"];\n/**\n * @file tmpfiles.js Utilities for securely and portably creating temporary\n * files with NodeJS.\n * @author Wes Garland, wes@kingsds.network\n * @date April 2020\n */\n\nconst { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\nconst path = requireNative('path');\nconst fs = requireNative('fs');\nconst os = requireNative('os');\nconst process = requireNative('process');\nconst { assert } = __webpack_require__(/*! dcp/common/dcp-assert */ \"./src/common/dcp-assert.js\");\n\n/**\n * deterministic initializer ensures dcpConfig is ready\n */\nexports.getSystemTempDir = function tmpfiles$$getSystemTempDir()\n{\n if (!exports.hasOwnProperty('_systemTempDir'))\n {\n let systemTempDir = process.env.TMPDIR || (dcpConfig.system && dcpConfig.system.tempDir) || (process.platform === 'win32' && process.env.TEMP);\n if (!systemTempDir) {\n if (process.platform === 'win32') {\n if (process.env.LOCALAPPDATA) \n systemTempDir = path.join(systemTempDir, 'Temp');\n else if (process.env.HOMEDRIVE && process.env.HOMEPATH)\n systemTempDir = path.join(process.env.HOMEDRIVE + process.env.HOMEPATH, 'AppData', 'Local', 'Temp');\n else if (process.env.SystemRoot)\n systemTempDir = path.join(process.env.SystemRoot, 'Temp');\n else if (process.env.SystemDrive)\n systemTempDir = path.join(process.env.SystemDrive + '\\\\Windows', 'Temp');\n else\n systemTempDir = 'C:\\\\WINDOWS\\\\Temp'\n }\n else\n systemTempDir = '/tmp';\n }\n\n exports._systemTempDir = systemTempDir;\n }\n\n return exports._systemTempDir;\n}\n\n/**\n * Create a temporary thing in the filesystem.\n *\n * @param {function} createThingFn The function which creates the thing; must take as its argument\n * a fully-pathed filename and throw an exception failure. It must\n * return a handle to the thing, which will become the return value\n * of this function.\n * @param {string} basenamePattern A string specifying the file basename pattern. The\n * character X will be replaced with randomly-generated\n * characters, similar to mkstemp(3).\n * @param {string} extension [optional] The filename extension to use; false => none, falsey => dcp\n * @param {string} tempDir [optional] The directory in which to create the thing. Default is\n * exports.systemTempDir.\n *\n * @return a handle created by createThingFn\n */\nexports.createTempThing = function createTempThing(createThingFn, basenamePattern, extension, tempDir)\n{\n const generateEntropy = requireNative('nanoid').customAlphabet('1234567890abcdefghijklmnopqrstuvwyxzkABCDEFGHIJKLMNOPQRSTUVWXYZ', 64);\n\n var entropy = generateEntropy();\n var basename = '';\n var filename; /* The filename of the temp thing we are creating */\n var thing; /* The temp thing we are creating */\n var removed = false; /* Used by removeFn to avoid races */\n\n if (!tempDir)\n tempDir = exports.getSystemTempDir();\n \n if (!basenamePattern)\n basenamePattern = `dcp-${process.pid}-XXXXXXXX`;\n\n const re = /[xX]{3,}/;\n assert(re.test(basenamePattern))\n\n let tries = 0 \n loop: while ('goto considered harmful considered harmful') { // eslint-disable-line no-constant-condition\n let j = Math.floor(Math.random() * entropy.length);\n for (let i=0; i < basenamePattern.length; i++) {\n if (basenamePattern[i] === 'X')\n basename += entropy[j++ % entropy.length];\n else\n basename += basenamePattern[i];\n }\n\n if (extension !== false)\n basename += '.' + (extension || 'dcp');\n filename = path.join(tempDir, basename);\n if (fs.existsSync(filename)) {\n tries = tries++; \n basename='';\n if (tries > 1000)\n throw new Error('DCPU-1001'); /* this is ~impossible */\n continue loop;\n }\n try {\n thing = createThingFn(filename);\n } catch(e) {\n if (e.code === 'EEXIST')\n continue;\n throw e;\n }\n break loop;\n }\n\n function removeFn()\n {\n process.off('exit', removeFn);\n if (!process.env.DCP_DEBUG_TMP_FILES && !removed)\n {\n try\n {\n try\n {\n fs.unlinkSync(filename);\n removed = true;\n }\n catch(error)\n {\n if (fs.lstatSync(filename).isDirectory())\n fs.rmdirSync(filename, { recursive: true, maxRetries: 20 }); /* maxRetries might need rimraf? rtfm later /wg aug 2022 */\n else\n throw error;\n }\n }\n catch(error)\n {\n console.warn(`Warning: unable to clean up '${filename}'`, error.message);\n }\n }\n }\n \n if (os.platform() !== 'win32')\n process.on('exit', removeFn); /* Pipe namespace automatically cleaned up on win32 */\n\n return { thing, removeFn, filename };\n}\n\n/** \n * Securely create a temporary file in the system temp directory. A cleanup is registered to\n * automatically remove the file when the process is closed.\n * \n * @param {string} basenamePattern A string specifying the file basename pattern. The\n * character X will be replaced with randomly-generated\n * characters, similar to mkstemp(3).\n * @param {string} extension [optional] The filename extension to use; pass false for none\n * @param perms [optional] The permissions argument to openSync.\n *\n * @returns {object} An object with properties:\n * fd: the open file descriptor\n * filename: the name of the newly-created file\n * toString: method which returns the filename\n * write: method which writes to the file descriptor (like fs.write)\n * writeSync: method which writes to the file descriptor (like fs.writeSync)\n * close: method which closes the file descriptor (like fs.close)\n * closeSync: method which closes the file descriptor (like fs.closeSync)\n * chmod: method which changes the file mode (like fs.fchmod)\n * chmodSync: method which changes the file mode (like fs.fchmodSync)\n * remove: method which remove the file from disk and cleans up the registered\n * process.exit cleanup. Does not close the file descriptor!\n */\nexports.createTempFile = function utils$$createTempFile(basenamePattern, extension, perms) {\n const inspect = requireNative('util').inspect.custom;\n let f = fs.constants;\n let hnd;\n\n /** Atomically create a file using the permissions in the closed-over environment.\n * Atomic here means that either the file will be created or it won't be, and the\n * check-then-create race condition is handled by the OS.\n */\n function atomicCreateFile(filename) {\n return fs.openSync(filename, f.O_RDWR | f.O_CREAT | f.O_EXCL, perms | 0o600);\n }\n\n const { thing: fd, removeFn, filename } = exports.createTempThing(atomicCreateFile, basenamePattern, extension);\n\n hnd = { fd, filename };\n hnd.toString = () => filename;\n hnd.write = function tmp$write() { return fs.write .apply(null, [fd].concat(Array.from(arguments))) };\n hnd.writeSync = function tmp$writeSync() { return fs.writeSync .apply(null, [fd].concat(Array.from(arguments))) };\n hnd.chmod = function tmp$chmod() { return fs.fchmod .apply(null, [fd].concat(Array.from(arguments))) };\n hnd.chmodSync = function tmp$chmodSync() { return fs.fchmodSync.apply(null, [fd].concat(Array.from(arguments))) };\n hnd.close = function tmp$close() { fs.close .apply(null, [fd].concat(Array.from(arguments))); delete hnd.fd; return hnd };\n hnd.closeSync = function tmp$closeSync() { fs.closeSync .apply(null, [fd].concat(Array.from(arguments))); delete hnd.fd; return hnd };\n hnd.remove = removeFn;\n\n hnd[inspect] = () => '[Object utils$$tempFile(' + filename + ')]';\n \n return hnd;\n}\n\n/**\n * Securely create a temporary directory in the system temp directory. A cleanup is registered to\n * automatically remove the directory and its children when the process is closed.\n *\n * @param {string} basenamePattern A string specifying the file basename pattern. The\n * character X will be replaced with randomly-generated\n * characters, similar to mkstemp(3).\n * @param {string} extension [optional] The filename extension to use; pass false for none\n * @param perms [optional] The mode argument to mkdirSync\n * @param {string} tempDir [optional] The directory in which to create the directory.\n *\n * @returns {object} An object with properties:\n * fd: the open file descriptor\n * dirname: the name of the newly-created directory\n * toString: method which returns dirname\n * remove: method which removes the directory and its children\n * from disk and cleans up the registered process.exit cleanup.\n */\nexports.createTempDir = function utils$$createTempDir(basenamePattern, extension, perms, tempDir)\n{\n const inspect = requireNative('util').inspect.custom;\n\n function createDir(dirname)\n {\n var opts = { recursive: Boolean(tempDir) };\n\n if (typeof perms !== 'undefined')\n opts.mode = perms;\n \n fs.mkdirSync(dirname, opts);\n }\n \n const { thing: fd, removeFn, filename } = exports.createTempThing(createDir, basenamePattern, extension, tempDir);\n const hnd = { fd, dirname: filename };\n hnd.toString = () => filename;\n hnd.remove = removeFn;\n hnd[inspect] = () => '[Object utils$$tempDir(' + hnd.dirname + ')]';\n \n return hnd;\n}\n\n/** \n * Securely create a temporary socket (eg for IPC) in the system temp directory. A cleanup is registered to\n * automatically remove the file when the process is closed.\n * \n * @param {string} basenamePattern A string specifying the file basename pattern. The\n * character X will be replaced with randomly-generated\n * characters, similar to mkstemp(3).\n * @param {string} extension [optional] The filename extension to use; pass false for none\n * @param perms [optional] The permissions argument to openSync. Default is 0600;\n * pass false to use OS default (0666 & umask(2)).\n * @param {function} connectHandler [optional] The callback to invoke when the socket is connected to\n * @returns {object} a socket, created with net.createConnection, using a path argument\n */\nexports.createTempSocket = function utils$$createTempSocket(basenamePattern, extension, connectHandler, perms) {\n const inspect = requireNative('util').inspect.custom;\n var pipeDir;\n \n if (os.platform() === 'win32')\n pipeDir = '\\\\\\\\.\\\\pipe\\\\';\n\n function createSocket(filename)\n {\n const net = requireNative('net');\n var server = net.createServer(connectHandler);\n var socket = server.listen(filename); /* creates the socket (named pipe) */\n\n socket.unref();\n\n return net.createConnection(filename);\n }\n\n const { thing: socket, removeFn, filename } = exports.createTempThing(createSocket, basenamePattern, extension, pipeDir);\n assert(os.platform() !== 'win32' || filename.startsWith('\\\\\\\\.\\\\pipe\\\\'));\n\n if (!socket)\n throw new Error(`Could not create socket ${filename || 'like ' + basenamePattern + '.' + extension}`); /* this should be impossible */\n\n if (filename && perms !== false) {\n try {\n fs.chmodSync(filename, perms || 0o600);\n } catch(error) {\n if (socket.close)\n socket.close();\n console.log(`Could not change permissions on named pipe ${filename} (${error.code || error.message}); closing`);\n throw error;\n }\n }\n\n socket[inspect] = () => '[Object utils$$tempSocket(' + filename + ')]';\n socket.on('close', removeFn);\n return socket;\n}\n\n/**\n * Copy the entire source file into the target, appending at the current\n * file offset. If source file has been closed, we will re-open for the\n * the duration of this call in read-only mode. This routine does not attempt\n * to preserve the file position of the input file.\n *\n * @param source {object} The file to copy, handle from createTempFile\n * @param target {object} The file to append to, handle from createTempFile\n * @param target2 {object} An optioanl stream which also receives the same \n * data via its write method. For npm create-hash.\n */ \nexports.catFile = function utils$$catFile(source, target, target2) {\n var buf = Buffer.allocUnsafe(8192 * 64);\n var pos = 0;\n var nRead;\n var sourceFd = source.fd;\n var targetFd = target.fd;\n\n if (!sourceFd && sourceFd !== 0) {\n sourceFd = fs.openSync(source.filename, fs.O_RDONLY, 0o400);\n }\n \n do {\n nRead = fs.readSync(sourceFd, buf, 0, buf.length, pos);\n fs.writeSync(targetFd, buf.slice(0, nRead));\n pos += nRead;\n if (target2)\n target2.write(buf);\n } while(nRead === buf.length);\n\n if (!source.fd && sourceFd !== 0) {\n fs.closeSync(sourceFd);\n }\n}\n\n\n//# sourceURL=webpack://dcp/./src/utils/tmpfiles.js?");
4908
5049
 
4909
5050
  /***/ }),
4910
5051
 
@@ -5678,7 +5819,7 @@ eval("module.exports = JSON.parse('{\"1.3.132.0.10\":\"secp256k1\",\"1.3.132.0.3
5678
5819
  /***/ ((module) => {
5679
5820
 
5680
5821
  "use strict";
5681
- eval("module.exports = JSON.parse('{\"browser\":[\"deny-node\",\"kvin/kvin.js\",\"script-load-wrapper\",\"wrap-event-listeners\",\"event-loop-virtualization\",\"gpu-timers\",\"access-lists\",\"bravojs-init\",\"bravojs/bravo.js\",\"bravojs-env\",\"calculate-capabilities\",\"bootstrap\"],\"node\":[\"kvin/kvin.js\",\"sa-ww-simulation\",\"script-load-wrapper\",\"wrap-event-listeners\",\"event-loop-virtualization\",\"gpu-timers\",\"access-lists\",\"bravojs-init\",\"bravojs/bravo.js\",\"bravojs-env\",\"calculate-capabilities\",\"bootstrap\"],\"native\":[\"deny-node\",\"kvin/kvin.js\",\"sa-ww-simulation\",\"script-load-wrapper\",\"native-event-loop\",\"wrap-event-listeners\",\"event-loop-virtualization\",\"webgpu-worker-environment\",\"gpu-timers\",\"access-lists\",\"bravojs-init\",\"bravojs/bravo.js\",\"bravojs-env\",\"calculate-capabilities\",\"bootstrap\"],\"webGpuNative\":[\"deny-node\",\"kvin/kvin.js\",\"bootstrap\"],\"nodeTesting\":[\"kvin/kvin.js\",\"sa-ww-simulation\",\"script-load-wrapper\",\"wrap-event-listeners\",\"event-loop-virtualization\",\"gpu-timers\",\"access-lists\",\"bravojs-init\",\"bravojs/bravo.js\",\"bravojs-env\",\"calculate-capabilities\",\"bootstrap\",\"testing.js\"],\"testing\":[\"deny-node\",\"kvin/kvin.js\",\"sa-ww-simulation\",\"script-load-wrapper\",\"native-event-loop\",\"wrap-event-listeners\",\"event-loop-virtualization\",\"webgpu-worker-environment\",\"gpu-timers\",\"access-lists\",\"bravojs-init\",\"bravojs/bravo.js\",\"bravojs-env\",\"calculate-capabilities\",\"bootstrap\",\"testing.js\"]}');\n\n//# sourceURL=webpack://dcp/./node_modules/dcp-client/generated/sandbox-definitions.json?");
5822
+ eval("module.exports = JSON.parse('{\"browser\":[\"deny-node\",\"kvin/kvin.js\",\"script-load-wrapper\",\"wrap-event-listeners\",\"event-loop-virtualization\",\"gpu-timers\",\"access-lists\",\"bravojs-init\",\"bravojs/bravo.js\",\"bravojs-env\",\"calculate-capabilities\",\"bootstrap\"],\"node\":[\"kvin/kvin.js\",\"sa-ww-simulation\",\"script-load-wrapper\",\"wrap-event-listeners\",\"event-loop-virtualization\",\"gpu-timers\",\"access-lists\",\"bravojs-init\",\"bravojs/bravo.js\",\"bravojs-env\",\"calculate-capabilities\",\"bootstrap\"],\"native\":[\"deny-node\",\"kvin/kvin.js\",\"sa-ww-simulation\",\"script-load-wrapper\",\"native-event-loop\",\"wrap-event-listeners\",\"event-loop-virtualization\",\"gpu-timers\",\"access-lists\",\"bravojs-init\",\"bravojs/bravo.js\",\"bravojs-env\",\"calculate-capabilities\",\"bootstrap\"],\"webGpuNative\":[\"deny-node\",\"kvin/kvin.js\",\"sa-ww-simulation\",\"script-load-wrapper\",\"native-event-loop\",\"wrap-event-listeners\",\"event-loop-virtualization\",\"webgpu-worker-environment\",\"gpu-timers\",\"access-lists\",\"bravojs-init\",\"bravojs/bravo.js\",\"bravojs-env\",\"calculate-capabilities\",\"bootstrap\"],\"nodeTesting\":[\"kvin/kvin.js\",\"sa-ww-simulation\",\"script-load-wrapper\",\"wrap-event-listeners\",\"event-loop-virtualization\",\"gpu-timers\",\"access-lists\",\"bravojs-init\",\"bravojs/bravo.js\",\"bravojs-env\",\"calculate-capabilities\",\"bootstrap\",\"testing.js\"],\"testing\":[\"deny-node\",\"kvin/kvin.js\",\"sa-ww-simulation\",\"script-load-wrapper\",\"native-event-loop\",\"wrap-event-listeners\",\"event-loop-virtualization\",\"gpu-timers\",\"access-lists\",\"bravojs-init\",\"bravojs/bravo.js\",\"bravojs-env\",\"calculate-capabilities\",\"bootstrap\",\"testing.js\"]}');\n\n//# sourceURL=webpack://dcp/./node_modules/dcp-client/generated/sandbox-definitions.json?");
5682
5823
 
5683
5824
  /***/ }),
5684
5825