dcp-client 4.3.0-3 → 4.3.0-5
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.
|
@@ -3967,7 +3967,7 @@ eval("// Copyright Joyent, Inc. and other Node contributors.\n//\n// Permission
|
|
|
3967
3967
|
\**********************/
|
|
3968
3968
|
/***/ ((__unused_webpack_module, exports, __webpack_require__) => {
|
|
3969
3969
|
|
|
3970
|
-
eval("/**\n * @file build.js\n * Provide build information for DCP rtlink code, the same way as the dcp/build\n * module is injected for dcp-client code. That is what src/common/dcp-build.js\n * was supposed to do. We collide symbols here with a dcp-client built-in, so\n * that the code works the same everywhere but a gives different results depending\n * on how it was linked.\n *\n * This file is expressly processed by the copy-stamp rule in the core install\n * map, so that the symbols in here get substituted with their 'real' values as\n * the product is installed. A side effect of this is that dev versions running\n * out of the source tree will have symbol names instead of symbol values here.\n *\n * @author Wes Garland, wes@distributive.network\n * @date Nov 2022\n */\n\nexports.version = '
|
|
3970
|
+
eval("/**\n * @file build.js\n * Provide build information for DCP rtlink code, the same way as the dcp/build\n * module is injected for dcp-client code. That is what src/common/dcp-build.js\n * was supposed to do. We collide symbols here with a dcp-client built-in, so\n * that the code works the same everywhere but a gives different results depending\n * on how it was linked.\n *\n * This file is expressly processed by the copy-stamp rule in the core install\n * map, so that the symbols in here get substituted with their 'real' values as\n * the product is installed. A side effect of this is that dev versions running\n * out of the source tree will have symbol names instead of symbol values here.\n *\n * @author Wes Garland, wes@distributive.network\n * @date Nov 2022\n */\n\nexports.version = '8a95bd250c03421b473f543c2e9c7a85516d622c';\nexports.branch = 'prod-20230922';\n\n/* When/how configure.sh ran */\nexports.config = __webpack_require__(/*! ../etc/local-config.json */ \"./etc/local-config.json\");\ndelete exports.config.install;\n\n/* When/how install.sh ran (not for dcp-client) */\nexports.install = {\n timestamp: new Date(Number('__TIMESTAMP__') * 1000),\n build: '__DCP_BUILD',\n};\n\n\n\n\n//# sourceURL=webpack://dcp/./src/build.js?");
|
|
3971
3971
|
|
|
3972
3972
|
/***/ }),
|
|
3973
3973
|
|
|
@@ -4019,7 +4019,7 @@ eval("/**\n * @file dcp-assert.js\n * Simple assertion modul
|
|
|
4019
4019
|
\*********************************/
|
|
4020
4020
|
/***/ ((__unused_webpack_module, exports, __webpack_require__) => {
|
|
4021
4021
|
|
|
4022
|
-
eval("/* provided dependency */ var process = __webpack_require__(/*! ./node_modules/process/browser.js */ \"./node_modules/process/browser.js\");\n/**\n * @file dcp-build.js Return an object describing the current DCP build.\n * @author Ryan Rossiter <ryan@kingsds.network>\n * @author Wes Garland, wes@distributive.network\n * @date July 2020\n * @date Apr 2023\n */\n\nif ((__webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\").platform) === 'nodejs')\n{\n const { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\n const fs = requireNative('fs');\n const path = requireNative('path');\n\n try\n {\n const rtlink = requireNative('dcp-rtlink');\n const localConfig = false\n || process.env.DCP_LOCAL_CONFIG_JSON /* dcp-client bundler's temp dir */\n || path.join(rtlink.runningLocation, 'etc', 'local-config.json'); /* installed or src dir config */\n Object.assign(exports, JSON.parse(fs.readFileSync(localConfig), 'utf-8'));\n }\n catch(error)\n {\n if (error.code !== 'MODULE_NOT_FOUND')\n throw error;\n /* If we arrive here, we couldn't resolve dcp-rtlink => we're on dcp-client */\n Object.assign(exports, {\"version\":\"
|
|
4022
|
+
eval("/* provided dependency */ var process = __webpack_require__(/*! ./node_modules/process/browser.js */ \"./node_modules/process/browser.js\");\n/**\n * @file dcp-build.js Return an object describing the current DCP build.\n * @author Ryan Rossiter <ryan@kingsds.network>\n * @author Wes Garland, wes@distributive.network\n * @date July 2020\n * @date Apr 2023\n */\n\nif ((__webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\").platform) === 'nodejs')\n{\n const { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\n const fs = requireNative('fs');\n const path = requireNative('path');\n\n try\n {\n const rtlink = requireNative('dcp-rtlink');\n const localConfig = false\n || process.env.DCP_LOCAL_CONFIG_JSON /* dcp-client bundler's temp dir */\n || path.join(rtlink.runningLocation, 'etc', 'local-config.json'); /* installed or src dir config */\n Object.assign(exports, JSON.parse(fs.readFileSync(localConfig), 'utf-8'));\n }\n catch(error)\n {\n if (error.code !== 'MODULE_NOT_FOUND')\n throw error;\n /* If we arrive here, we couldn't resolve dcp-rtlink => we're on dcp-client */\n Object.assign(exports, {\"version\":\"8a95bd250c03421b473f543c2e9c7a85516d622c\",\"branch\":\"prod-20230922\",\"dcpClient\":{\"version\":\"4.3.0\",\"resolved\":\"git+ssh://git@gitlab.com/Distributed-Compute-Protocol/dcp-client.git#2ba1f35c876d61c52ffec4f5c67f2c5c8449ec5c\",\"overridden\":false},\"built\":\"Tue Sep 26 2023 09:10:57 GMT-0400 (Eastern Daylight Saving Time)\",\"config\":{\"generated\":\"Tue 26 Sep 2023 09:10:56 AM EDT by erose on lorge\",\"build\":\"debug\"},\"webpack\":\"5.88.2\",\"node\":\"v18.17.1\"}.config);\n }\n}\nelse\n{\n /* For all non-node platforms, just assume that we're also on dcp-client */\n Object.assign(exports, {\"version\":\"8a95bd250c03421b473f543c2e9c7a85516d622c\",\"branch\":\"prod-20230922\",\"dcpClient\":{\"version\":\"4.3.0\",\"resolved\":\"git+ssh://git@gitlab.com/Distributed-Compute-Protocol/dcp-client.git#2ba1f35c876d61c52ffec4f5c67f2c5c8449ec5c\",\"overridden\":false},\"built\":\"Tue Sep 26 2023 09:10:57 GMT-0400 (Eastern Daylight Saving Time)\",\"config\":{\"generated\":\"Tue 26 Sep 2023 09:10:56 AM EDT by erose on lorge\",\"build\":\"debug\"},\"webpack\":\"5.88.2\",\"node\":\"v18.17.1\"}.config);\n}\n\nif (typeof exports.build === 'undefined') {\n throw new Error('Could not determine build type!');\n}\n\n\n//# sourceURL=webpack://dcp/./src/common/dcp-build.js?");
|
|
4023
4023
|
|
|
4024
4024
|
/***/ }),
|
|
4025
4025
|
|
|
@@ -4293,7 +4293,7 @@ eval("/**\n * @file password.js\n * Modal providing a way to
|
|
|
4293
4293
|
\**********************************************/
|
|
4294
4294
|
/***/ ((__unused_webpack_module, exports, __webpack_require__) => {
|
|
4295
4295
|
|
|
4296
|
-
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=
|
|
4296
|
+
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=8a95bd250c03421b473f543c2e9c7a85516d622c'; /* 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?");
|
|
4297
4297
|
|
|
4298
4298
|
/***/ }),
|
|
4299
4299
|
|
|
@@ -4325,7 +4325,7 @@ eval("/**\n * @file Module that implements Compute API\n * @module dcp/comput
|
|
|
4325
4325
|
/***/ ((module, exports, __webpack_require__) => {
|
|
4326
4326
|
|
|
4327
4327
|
"use strict";
|
|
4328
|
-
eval("/* module decorator */ module = __webpack_require__.nmd(module);\n/* provided dependency */ var process = __webpack_require__(/*! ./node_modules/process/browser.js */ \"./node_modules/process/browser.js\");\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 if (process.env.DCP_CONFIG_USE_DEPRECATED_FUTURE)\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 /* 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\":\"
|
|
4328
|
+
eval("/* module decorator */ module = __webpack_require__.nmd(module);\n/* provided dependency */ var process = __webpack_require__(/*! ./node_modules/process/browser.js */ \"./node_modules/process/browser.js\");\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 if (process.env.DCP_CONFIG_USE_DEPRECATED_FUTURE)\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 /* 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\":\"8a95bd250c03421b473f543c2e9c7a85516d622c\",\"branch\":\"prod-20230922\",\"dcpClient\":{\"version\":\"4.3.0\",\"resolved\":\"git+ssh://git@gitlab.com/Distributed-Compute-Protocol/dcp-client.git#2ba1f35c876d61c52ffec4f5c67f2c5c8449ec5c\",\"overridden\":false},\"built\":\"Tue Sep 26 2023 09:10:57 GMT-0400 (Eastern Daylight Saving Time)\",\"config\":{\"generated\":\"Tue 26 Sep 2023 09:10:56 AM EDT by erose on lorge\",\"build\":\"debug\"},\"webpack\":\"5.88.2\",\"node\":\"v18.17.1\"},\n 'dcp-xhr': __webpack_require__(/*! ../common/dcp-xhr */ \"./src/common/dcp-xhr.js\"),\n 'dcp-env': __webpack_require__(/*! ../common/dcp-env */ \"./src/common/dcp-env.js\"),\n 'dcp-url': __webpack_require__(/*! ../common/dcp-url */ \"./src/common/dcp-url.js\"),\n 'cli': __webpack_require__(/*! ../common/cli */ \"./src/common/cli.js\"),\n 'dcp-timers': __webpack_require__(/*! ../common/dcp-timers */ \"./src/common/dcp-timers.js\"),\n 'dcp-dot-dir': __webpack_require__(/*! ../common/dcp-dot-dir */ \"./src/common/dcp-dot-dir.js\"),\n 'dcp-assert': __webpack_require__(/*! ../common/dcp-assert */ \"./src/common/dcp-assert.js\"),\n 'dcp-events': __webpack_require__(/*! ../common/dcp-events */ \"./src/common/dcp-events/index.js\"),\n 'utils': __webpack_require__(/*! ../utils */ \"./src/utils/index.js\"),\n 'debugging': __webpack_require__(/*! ../debugging */ \"./src/debugging.js\"),\n 'publish': __webpack_require__(/*! ../common/dcp-publish */ \"./src/common/dcp-publish.js\"),\n 'compute-groups': {\n ...__webpack_require__(/*! ./compute-groups */ \"./src/dcp-client/compute-groups/index.js\"),\n publicGroupOpaqueId: (__webpack_require__(/*! ../common/scheduler-constants */ \"./src/common/scheduler-constants.js\").computeGroups[\"public\"].opaqueId),\n },\n 'bank-util': __webpack_require__(/*! ./bank-util */ \"./src/dcp-client/bank-util.js\"),\n 'protocol-v4': __webpack_require__(/*! ../protocol-v4 */ \"./src/protocol-v4/index.js\"), /* deprecated */\n 'client-modal': __webpack_require__(/*! ./client-modal */ \"./src/dcp-client/client-modal/index.js\"),\n 'eth': __webpack_require__(/*! ./wallet/eth */ \"./src/dcp-client/wallet/eth.js\"),\n 'serialize': __webpack_require__(/*! ../utils/serialize */ \"./src/utils/serialize.js\"),\n 'kvin': __webpack_require__(/*! kvin */ \"./node_modules/kvin/kvin.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 'signal-handler': __webpack_require__(/*! ../node-libs/signal-handler */ \"./src/node-libs/signal-handler.js\"),\n 'standard-objects': {}\n }, 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 /* Export the constructors used by object literals and boxing objects. Usually these are the same as\n * the standard objects, but not always -- evaluation environments like node vm contexts can have\n * different standard objects supplied via the sandboxing object than the engine uses internally.\n */\n modules['engine-constructors'] = {\n Object: ({}).constructor,\n Array: ([]).constructor,\n Number: (0).constructor,\n String: ('').constructor,\n Boolean: (false).constructor,\n Function: (()=>1).constructor,\n };\n\n if (typeof BigInt !== 'undefined')\n {\n modules['standard-objects']['BigInt'] === BigInt;\n modules['engine-constructors'].BigInt = (0n).constructor;\n }\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 exports['dcp-default-config'] = {\"_serializeVerId\":\"v8\",\"what\":{\"ctr\":0,\"ps\":{\"dcp\":{\"ctr\":0,\"ps\":{\"connectionOptions\":{\"ctr\":0,\"ps\":{\"default\":{\"ctr\":0,\"ps\":{\"connectTimeout\":60,\"disconnectTimeout\":900,\"lingerTimeout\":18000,\"identityUnlockTimeout\":300,\"batchWaitTime\":0.03,\"ttl\":{\"raw\":{\"min\":15,\"max\":600,\"default\":120}},\"transports\":{\"arr\":[\"socketio\"]},\"socketio\":{\"raw\":{}}}}}},\"validitySlopValue\":10,\"validityStampCachePurgeInterval\":60,\"maxConnectionTimeout\":300000}},\"worker\":{\"raw\":{}},\"evaluator\":{\"ctr\":0,\"ps\":{\"listen\":{\"ctr\":\"dcpUrl$$DcpURL\",\"ps\":{},\"arg\":\"http://localhost:9000/\"},\"location\":{\"ctr\":\"dcpUrl$$DcpURL\",\"ps\":{},\"arg\":\"http://localhost:9000/\"},\"friendLocation\":{\"seen\":6}}},\"supervisor\":{\"raw\":{\"dcp\":{\"connectionOptions\":{\"default\":{\"identityUnlockTimeout\":900}}},\"tuning\":{\"watchdogInterval\":7,\"minSandboxStartDelay\":0.1,\"maxSandboxStartDelay\":0.7,\"minSandboxSlack\":0.2,\"maxSandboxSlack\":0.5,\"maxSandboxSliceRetries\":1,\"cachedJobsThreshold\":0,\"prefetchInterval\":30,\"pruneFrequency\":30000,\"mustPruneMultiplier\":1.3,\"defaultDelayIncrement\":50,\"maxExtraSandboxes\":8,\"maxResultSubmissionRetries\":3,\"reservedSliceLifetime\":300000},\"repoMan\":{\"frequency\":60000,\"thresholdMultiplier\":2},\"sandbox\":{\"generalTimeout\":6000,\"sliceTimeout\":86400000,\"progressTimeout\":300000,\"progressReportInterval\":1200000,\"maxSandboxUse\":1000},\"pCores\":0}},\"standaloneWorker\":{\"undefined\":true},\"scheduler\":{\"ctr\":0,\"ps\":{\"location\":{\"ctr\":\"dcpUrl$$DcpURL\",\"ps\":{},\"arg\":\"https://scheduler.distributed.computer/\"},\"worker\":{\"ctr\":0,\"ps\":{\"types\":{\"arr\":[\"v4\"]},\"operations\":{\"raw\":\"1.0.0\"}}},\"compatibility\":{\"raw\":{\"minimum\":{\"dcp\":\"^5.0.0\",\"dcp-client\":\"^4.0.0\",\"dcp-worker\":\"^2.1.0\",\"operations\":{\"work\":\"^4.1.0\",\"compute\":\"^1.0.0\",\"bank\":\"^4.0.0\"}},\"exclusions\":{}}}}}}}};\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?");
|
|
4329
4329
|
|
|
4330
4330
|
/***/ }),
|
|
4331
4331
|
|
|
@@ -4346,7 +4346,7 @@ eval("/**\n * @file job-values.js\n * Utility code related t
|
|
|
4346
4346
|
/***/ ((__unused_webpack_module, exports, __webpack_require__) => {
|
|
4347
4347
|
|
|
4348
4348
|
"use strict";
|
|
4349
|
-
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// @ts-check\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 { serializeJobValue, 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\");\nconst { setTimeout } = __webpack_require__(/*! dcp/common/dcp-timers */ \"./src/common/dcp-timers.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/** @typedef {import('dcp/dcp-client/range-object').RangeLike} RangeLike */\n/** @typedef {import('dcp/dcp-client/wallet/keystore').Keystore} Keystore */\n/** @typedef {import('dcp/utils/jsdoc-types').SupervisorOptions} SupervisorOptions */\n/** @typedef {import('dcp/common/dcp-events').PropagatingEventEmitter} PropagatingEventEmitter */\n/** @typedef {import('scheduler-v4/libexec/job-submit/operations/submit').MarshaledInputData} MarshaledInputData */\n\n/**\n * Ensure input data is an appropriate format\n * @param {RangeObject | DistributionRange | RemoteDataSet | Array | RangeLike | Iterable}\n * inputData - A URI-shaped string, a [Multi]RangeObject-constructing value, or\n * an array of slice data\n * @return {RangeObject | DistributionRange | RemoteDataSet | Array | RangeLike | Iterable}\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 {PropagatingEventEmitter}\n * @hideconstructor\n * @access public\n */\nclass Job extends PropagatingEventEmitter\n{\n /* Private class fields */\n #jobDeployRef; /* Event loop reference to the job deploy cyle. Different from Job.ref() which will reference the job. */\n #jobRef; /* Keep the job alive. */\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\n /* A default reference the Job is set. */\n this.ref();\n\n if (typeof arguments[0] === 'function')\n arguments[0] = arguments[0].toString();\n\n if (typeof arguments[0] === 'string')\n {\n const { encodeDataURI } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\n this.workFunctionURI = encodeDataURI(arguments[0], 'application/javascript');\n } \n else if (DcpURL.isURL(arguments[0]))\n this.workFunctionURI = arguments[0].href;\n\n this.jobInputData = wrangleData(arguments[1] || []);\n this.jobArguments = wrangleData(arguments[2] || []);\n \n log('num wrangledInputData:', this.jobInputData.length);\n log('num wrangledArguments:', this.jobArguments.length);\n\n this.initEventSystems();\n\n /**\n * An object describing the cost the user believes each the average slice will incur, in terms of CPU/GPU and I/O.\n * If defined, this object is used to provide initial scheduling hints and to calculate escrow amounts.\n *\n * @type {object}\n * @access public\n */\n this.initialSliceProfile = undefined;\n \n // The max value that the client is willing to spend to deploy\n // (list on the scheduler, doesn't include compute payment)\n // maxDeployPayment is the max the user is willing to pay to DCP (as a\n // Hold), in addition to the per-slice offer and associated scrape.\n // Currently calculated as `deployCost = costPerKB *\n // (JSON.stringify(job).length / 1024) // 1e-9 per kb`\n // @todo: figure this out / er nov 2018\n this.maxDeployPayment = 1;\n\n /**\n * An object describing the requirements that workers must have to be eligible for this job. The default values are set in the job-submitter, and only the client specified\n * requirements are sent over the wire. See {@link https://docs.dcp.dev/specs/compute-api.html#requirements-objects|Requirements Objects}.\n *\n * @type {object}\n * @access public\n */\n this.requirements = {};\n\n /**\n * A place to store public-facing attributes of the job. Anything stored on this object will be available inside the work \n * function (see {@link module:dcp/compute~sandboxEnv.work}). The properties documented here may be used by workers to display what jobs are currently being \n * worked on.\n * @access public\n * @property {string} name Public-facing name of this job.\n * @property {string} description Public-facing description for this job.\n * @property {string} link Public-facing link to external resource about this job.\n */\n this.public = {\n name: null,\n description: null,\n link: null,\n };\n \n /**\n * A cryptographic receipt indicating deployment of the job on the scheduler\n * @type {object}\n * @access public\n */\n this.receipt = null;\n \n /**\n * a SliceProfile object which contains the average costs for the slices which have been computed to date.\n * Until the first result is returned, this property is undefined.\n * @type {object}\n * @access public\n */\n this.meanSliceProfile = null;\n \n /**\n * A number (can be null, undefined, or infinity) describing the estimationSlicesRemaining in the jpd (dcp-2593)\n * @type {number}\n * @access public\n */\n this.estimationSlices = undefined;\n /**\n * When true, allows a job in estimation to have requestTask return multiple estimation slices.\n * This flag applies independent of infinite estimation, viz., this.estimationSlices === null .\n * @type {boolean}\n * @access public\n */\n this.greedyEstimation = false;\n /**\n * tunable parameters per job\n * @access public\n * @param {object} tuning \n * @param {string} tuning.kvin Encode the TypedArray into a string, trying multiple methods to determine optimum \n * size/performance. The this.tune variable affects the behavior of this code this:\n * @param {boolean} speed If true, only do naive encoding: floats get represented as byte-per-digit strings\n * @param {boolean} size If true, try the naive, ab8, and ab16 encodings; pick the smallest\n * If both are false try the naive encoding if under typedArrayPackThreshold and use if smaller\n * than ab8; otherwise, use ab8\n */\n this.tuning = {\n kvin: {\n size: false,\n speed: false,\n },\n }\n /* For API interface to end-users only */\n Object.defineProperty(this, 'id', {\n get: () => this.address,\n set: (id) => { this.address = id }\n });\n\n this.uuid = uuidv4(); /** @see {@link https://kingsds.atlassian.net/browse/DCP-1475?atlOrigin=eyJpIjoiNzg3NmEzOWE0OWI4NGZkNmI5NjU0MWNmZGY2OTYzZDUiLCJwIjoiaiJ9|Jira Issue} */\n this.dependencies = []; /* dependencies of the work function */\n this.requirePath = []; /* require path for dependencies */\n this.modulePath = []; /* path to module that invoked .exec() for job */\n this.connected = false; /* true when exec or resume called */\n this.results = new ResultHandle(this); /* result handle */\n this.collateResults = true; /* option to receive results as they are computed & ensure all are received on finish */\n this.contextId = null; /* optional string which is used to indicate to caching mechanisms different keystores with same name */ \n this.force100pctCPUDensity = false; /* tell scheduler to assume this job uses 100% cpu density */\n this.workerConsole = false; /* tell workers to log more information about failures in the evaluator this job causes */\n this.address = null; /* job address, created by scheduler during exec call. */\n this.paymentAccountKeystore = null; /* keystore for payment for job to come from */\n this.status = { /* job status details */\n runStatus: null,\n total: null,\n distributed: null,\n computed: null\n };\n\n // service locations\n this.scheduler = dcpConfig.scheduler.services.jobSubmit.location;\n this.bank = dcpConfig.bank.services.bankTeller.location;\n\n // Compute groups. Add to public compute group by default\n this.computeGroups = [ Object.assign({}, schedulerConstants.computeGroups.public) ];\n\n // A Worker object that describes the local worker if one is used for a job.\n this.localWorker = {\n /** @type {object[]} */\n origins: [],\n originManager: {\n /** \n * @param {string} origin\n * @param {string | null} purpose \n * @param {string | null} key\n */\n add: (origin, purpose, key = null) => {\n this.localWorker.origins.push({\n origin: origin,\n purpose: purpose,\n key: key,\n });\n },\n },\n };\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 // Careful -- this.schedulerConnection does not exist.\n const response = await this.schedulerConnection.request('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.request('embiggenFeeStructure', {\n feeStructureAddress: this[INTERNAL_SYMBOL].payloadDetails.feeStructureId,\n additionalEscrow: new BigNumber(fundsRequired),\n fromAddress: this.paymentAccountKeystore.address,\n }, this.paymentAccountKeystore);\n\n bankConnection.close();\n const escrowReceipt = response.payload;\n return escrowReceipt;\n }\n\n /**\n * create bundles for local dependencies\n */\n _pack ()\n {\n var retval = (__webpack_require__(/*! ./node-modules */ \"./src/dcp-client/job/node-modules.js\").createModuleBundle)(this.dependencies);\n return retval;\n }\n\n /** \n * Collect all of the dependencies together, throw them into a BravoJS\n * module which sideloads them as a side effect of declaration, and transmit\n * them to the package manager. Then we return the package descriptor object,\n * which is guaranteed to have only one file in it.\n *\n * @returns {object} with properties name and files[0]\n */\n async _publishLocalModules()\n {\n const dcpPublish = __webpack_require__(/*! dcp/common/dcp-publish */ \"./src/common/dcp-publish.js\");\n \n const { tempFile, hash, unresolved } = await this._pack();\n\n if (!tempFile) {\n return { unresolved };\n }\n\n const sideloaderFilename = tempFile.filename;\n const pkg = {\n name: `dcp-pkg-v1-localhost-${hash.toString('hex')}`,\n version: '1.0.0',\n files: {\n [sideloaderFilename]: `${sideloaderModuleIdentifier}.js`,\n },\n }\n\n await dcpPublish.publish(pkg);\n tempFile.remove();\n\n return { pkg, unresolved };\n }\n \n /**\n * This function specifies a module dependency (when the argument is a string)\n * or a list of dependencies (when the argument is an array) of the work\n * function. This function can be invoked multiple times before deployment.\n * @param {string | string[]} modulePaths - A string or array describing one\n * or more dependencies of the job.\n * @access public\n */\n requires (modulePaths)\n {\n if (typeof modulePaths !== 'string' && (!Array.isArray(modulePaths) || modulePaths.some((modulePath) => typeof modulePath !== 'string')))\n throw new TypeError('The argument to dependencies is not a string or an array of strings');\n else if (modulePaths.length === 0)\n throw new RangeError('The argument to dependencies cannot be an empty string or array');\n else if (Array.isArray(modulePaths) && modulePaths.some((modulePath) => modulePath.length === 0))\n throw new RangeError('The argument to dependencies cannot be an array containing an empty string');\n\n if (!Array.isArray(modulePaths))\n modulePaths = [modulePaths];\n\n for (const modulePath of modulePaths)\n {\n if (modulePath[0] !== '.' && modulePath.indexOf('/') !== -1)\n {\n const modulePrefixRegEx = /^(.*)\\/.*?$/;\n const [, modulePrefix] = modulePath.match(modulePrefixRegEx);\n if (modulePrefix && this.requirePath.indexOf(modulePrefix) === -1)\n this.requirePath.push(modulePrefix);\n }\n this.dependencies.push(modulePath);\n }\n }\n \n /** Set the account upon which funds will be drawn to pay for the job.\n * @param {module:dcp/wallet.BankAccountKeystore} 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('Argument must be an instance of Keystore');\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 this.resultStorageDetails = new URL(locationUrl);\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('postParams must be an object');\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 this.localExecAllowedOrigins = {\n any: [],\n fetchData: [],\n fetchWorkFunctions: [],\n fetchArguments: [],\n sendResults: [],\n };\n\n let worker;\n this.on('accepted', () => {\n // Fill in localexec-specific requirements to config\n /** @type {SupervisorOptions} */\n const config = {\n trustComputeGroupOrigins: false,\n allowOrigins: this.localExecAllowedOrigins,\n paymentAddress: this.identityKeystore.address,\n allowConsoleAccess: true,\n jobAddresses: [this.address],\n cores: { cpu: cores, gpu: undefined },\n sandboxOptions: {\n ignoreNoProgress: true,\n SandboxConstructor: (DCP_ENV.platform === 'nodejs' && (__webpack_require__(/*! ../worker/evaluators */ \"./src/dcp-client/worker/evaluators/index.js\").nodeEvaluatorFactory)()),\n },\n };\n\n if (this.localWorker?.origins?.length)\n {\n for (let i = 0; i < this.localWorker.origins.length; i++) \n {\n const originToAdd = this.localWorker.origins[i];\n\n /* Push origins to localExecAllowedOrigins. If a null purpose is provided, \n push it onto the 'any' array of this.localExecAllowedOrigins */\n this.localExecAllowedOrigins[(originToAdd.purpose === null ? 'any' : originToAdd.purpose)].push(originToAdd.origin);\n }\n }\n\n config.cores = Worker.defaultCores(config);\n worker = new Worker(this.identityKeystore, config);\n\n // @ts-ignore\n this.localWorker = worker;\n\n worker.on('error', (error) => {\n console.error('LocalExec Error:', error);\n });\n\n worker.on('warning', (warning) => {\n console.warn('LocalExec Warning:', warning);\n });\n\n worker.start().catch((e) => {\n console.error('Failed to start worker for localExec:');\n console.error(e.message);\n });\n });\n\n if (DCP_ENV.platform === 'nodejs')\n {\n // Determine type of input data\n // marshalInputData actually returns the union of dataRange, dataValues, dataPattern, sliceCount,\n // but there isn't a simple way to express that in Javascript.\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 inputSetURIs.push(dataValues[i]);\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 // 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 {\n // Coerce into a URI to handle OS specific path separators.\n const inputFileURI = new URL(`file://${inputSetFiles[i].filename}`);\n this.localExecAllowedOrigins['fetchData'].push(inputFileURI.pathname);\n }\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 // data:URI - dont care about origins\n const workFunction = await fetchURI(this.workFunctionURI, false);\n workFunctionFile.writeSync(workFunction);\n \n const workFunctionFileURL = new URL('file://' + workFunctionFile);\n this.workFunctionURI = workFunctionFileURL.href;\n this.localExecAllowedOrigins['fetchWorkFunctions'].push(workFunctionFileURL.pathname);\n }\n \n let encodedJobArgumentUris = [];\n if (this.jobArguments)\n {\n if (this.jobArguments instanceof RemoteDataPattern) /* Not supported */\n throw new DCPError('Cannot use RemoteDataPattern as work function arguments', 'EBADARG')\n if (this.jobArguments instanceof RemoteDataSet) /* Entire set is RemoteDataSet */\n {\n this.jobArguments.forEach((e) =>\n {\n encodedJobArgumentUris.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 encodedJobArgumentUris.push(this.jobArguments[i]);\n else\n {\n if (this.jobArguments[i] instanceof RemoteDataSet) /* Member of set is RemoteDataSet */\n {\n this.jobArguments[i].forEach((e) =>\n {\n encodedJobArgumentUris.push(new URL(e));\n });\n }\n else /* Actual Value */\n {\n const localArgFile = createTempFile(`dcp-localExec-argument-${i}-XXXXXXXXX`, 'kvin');\n const localArgFileURI = new URL(`file://${localArgFile.filename}`);\n\n localArgFile.writeSync(JSON.stringify(kvinMarshal(this.jobArguments[i])));\n this.localExecAllowedOrigins['fetchArguments'].push(localArgFileURI.pathname);\n encodedJobArgumentUris.push(localArgFileURI);\n }\n }\n } \n }\n }\n this.marshaledArguments = kvinMarshal(encodedJobArgumentUris);\n }\n \n return this.exec(...args).finally(() => {\n if (worker) {\n const timerHandle = setTimeout(() => {\n // stop the worker\n worker.stop(true);\n }, 3000);\n // Allow workers and localExec to exit.\n if (timerHandle.unref)\n timerHandle.unref();\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\n if (this.connected)\n throw new Error(`Exec called twice on the same job handle (${this.address})`);\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'}.<br><br> 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 let msg = `Upload a DCP bank account keystore<br> containing compute credits.`;\n try\n {\n ks = await wallet.get({ contextId: this.contextId, jobName: this.public.name, msg});\n }\n catch (e)\n {\n if (e.code !== ClientModal.CancelErrorCode) throw e;\n };\n if (ks)\n {\n try\n {\n await ks.unlock(undefined, parseFloat(dcpConfig.job.maxDeployTime));\n locked = false;\n }\n catch (e)\n {\n // prompt user again if user enters password incorrectly, exit modal otherwise\n if (e.code !== wallet.unlockFailErrorCode) throw e;\n }\n }\n if (safety++ > 1000) throw new Error('EINFINITY: job.exec tried wallet.get more than 1000 times.')\n } while (locked);\n this.setPaymentAccountKeystore(ks)\n }\n \n // We either have a valid keystore + password or we have rejected by this point.\n if (!this.slicePaymentOffer)\n throw new Error('A payment profile must be assigned before executing the job');\n else\n this.feeStructure = this.slicePaymentOffer.toFeeStructure(this.jobInputData.length);\n\n if (!this.address)\n {\n try\n {\n this.readyStateChange('init');\n await this.deployJob();\n const listenersPromise = this.addInitialEvents();\n const computeGroupsPromise = this.joinComputeGroups();\n let uploadSlicePromise;\n // if job data is by value then upload data to the scheduler in a staggered fashion\n if (Array.isArray(this.dataValues) && !this.marshaledDataValues)\n {\n this.readyStateChange('uploading');\n uploadSlicePromise = addSlices(this.dataValues, this.address, tunedKvin)\n .then(() => {\n debugging('slice-upload') && console.info(`970: slice data uploaded, closing job...`);\n return this.close();\n });\n }\n \n // await all promises for operations that can be done after the job is deployed\n await Promise.all([listenersPromise, computeGroupsPromise, uploadSlicePromise]);\n \n this.readyStateChange('deployed');\n this.emit('accepted', this);\n this.#jobDeployRef = clearInterval(this.#jobDeployRef); /* Remove the reference to job deploy. */\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 this.startEventBandaidWatchdog();\n \n const onComplete = () => {\n this.stopEventBandaidWatchdog();\n resolve(this.results); \n };\n\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 this.stopEventBandaidWatchdog();\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 this.eventSubscriber.close();\n await computeGroups.closeServiceConnection().catch((err) => {\n console.error('Warning: could not close compute groups service connection', err);\n });\n })();\n\n /* Remove the reference to the Job. */\n this.unref();\n });\n }\n\n /**\n * Start the event-bandaid watchdog. Every [configurable] seconds, fetch\n * the job report and do appropriate things if the job is stopped.\n */\n startEventBandaidWatchdog()\n {\n if (this.eventBandaidHandle)\n this.stopEventBandaidWatchdog();\n\n this.eventBandaidHandle = setInterval(() => {\n this.eventBandaid();\n }, 60 * 1000);\n\n // if we are in a nodelike environment with unreffable timers, unref the\n // timer so it doesn't hold the process open. \n // \n // (n.b. while I understand that we _want_ the process to stay open while\n // the job lives, but this is Not The Way; the whole eventBandaid is a\n // temporary hack which will go away in time ~ER 2022-11-02)\n if (this.eventBandaidHandle.unref)\n this.eventBandaidHandle.unref();\n }\n\n /**\n * Stop and clean up the even-bandaid watchdog.\n */\n stopEventBandaidWatchdog()\n {\n if (this.eventBandaidHandle)\n clearInterval(this.eventBandaidHandle);\n\n this.eventBandaidHandle = false;\n }\n\n /**\n * Called on an interval, started by .exec and stopped by\n * onComplete/onCancel. Fetches the jobReport and verifies the job is\n * not in a terminal state; if the job is finished or cancelled, we\n * emit a synthetic internal stop event to trigger regular cleanup\n * (result retrieval, etc) and finalize the job handle.\n */\n eventBandaid()\n {\n this.getJobInfo()\n .then(jobInfo => {\n if ([jobStatus.finished, jobStatus.cancelled].includes(jobInfo.status))\n this.ee.emit('stopped', { runStatus: jobInfo.status });\n })\n .catch(error => {\n\n });\n }\n \n /**\n * job.addListeners(): Private function used to set up event listeners to the scheduler\n * before deploying the job.\n */\n async addInitialEvents ()\n {\n this.readyStateChange('listeners');\n\n // This is important: We need to flush the task queue before adding listeners\n // because we queue pending listeners by listening to the newListener event (in the constructor).\n // If we don't flush here, then the newListener events may fire after this function has run,\n // and the events won't be properly set up.\n await new Promise(resolve => setTimeout(resolve, 0));\n\n // @todo: Listen for an estimated cost, probably emit an \"estimated\" event when it comes in?\n // also @todo: Do the estimation task(s) on the scheduler and send an \"estimated\" event\n\n // Always listen to the stop event. It will resolve the work function promise, so is always needed.\n this.on('stop', (ev) => {this.ee.emit('stopped', ev)});\n\n // Connect listeners that were set up before exec\n if (this.desiredEvents.includes('result'))\n this.listeningForResults = true;\n await this.subscribeNewEvents(this.desiredEvents);\n\n // Connect listeners that are set up after exec\n this.on('newListener', (evt) => {\n if (evt === 'newListener' || this.desiredEvents.includes(evt))\n return;\n this.subscribeNewEvents([evt]);\n });\n \n // automatically add a listener for results if collateResults is on\n if (this.collateResults && !this.listeningForResults)\n this.on('result', () => {});\n\n debugging('dcp-client') && console.debug('subscribedEvents', this.desiredEvents);\n\n // If we have listeners for job.work, subscribe to custom events\n if (this.listenForCustomEvents)\n await this.subscribeCustomEvents();\n // Connect work event listeners that are set up after exec\n else\n this.work.on('newListener', () => this.subscribeCustomEvents());\n }\n \n /**\n * Subscribes to either reliable events or optional events. It is assumed that\n * any call to this function will include only new events.\n * @param {string[]} events \n */\n async subscribeNewEvents (events)\n {\n const reliableEvents = [];\n const optionalEvents = [];\n for (let eventName of events)\n {\n eventName = eventName.toLowerCase();\n if (this.eventTypes[eventName] && this.eventTypes[eventName].reliable)\n reliableEvents.push(eventName);\n else if (this.eventTypes[eventName] && !this.eventTypes[eventName].reliable)\n optionalEvents.push(eventName);\n else\n debugging('dcp-client') && console.debug(`Job handler has listener ${eventName} which isn't an event-router event.`);\n }\n if (debugging('dcp-client'))\n {\n console.debug('reliableEvents', reliableEvents);\n console.debug('optionalEvents', optionalEvents);\n }\n await this.eventSubscriber.subscribeManyEvents(reliableEvents, optionalEvents, { filter: { job: this.address } });\n }\n \n /**\n * Establishes listeners for worker events when requested by the client\n */\n async subscribeCustomEvents ()\n {\n if (!this.listeningForCustomEvents)\n await this.eventSubscriber.subscribeManyEvents([], ['custom'], { filter: { job: this.address } });\n this.listeningForCustomEvents = true\n }\n \n async joinComputeGroups ()\n {\n // localExec jobs are not entered in any compute group.\n if (!this.inLocalExec && this.computeGroups && this.computeGroups.length > 0)\n {\n this.readyStateChange('compute-groups');\n computeGroups.addRef(); // Just in case we're doing a Promise.all on multiple execs.\n\n // Add this job to its currently-defined compute groups (as well as public group, if included)\n let success;\n \n if (!Array.isArray(this.computeGroups)) \n throw new DCPError('Compute groups must be wrapped in an Array', 'DCPL-1101');\n\n for (let i = 0; i < this.computeGroups.length; i++)\n {\n let value = this.computeGroups[i];\n \n if (typeof value !== 'object')\n throw new DCPError(`This compute group: ${value[i]} must be an object`, 'DCPL-1102');\n \n if (value.joinKey && typeof value.joinKey !== 'string' && !(value.joinKey instanceof String))\n throw new DCPError(`This join key: ${value.joinKey} must be a string or a string literal`, 'DCPL-1103');\n else if (value.joinKeystore && !(value.joinKeystore instanceof wallet.Keystore))\n throw new DCPError(`This join Keystore: ${value.joinKeystore} must be an instance of wallet.Keystore`, 'DCPL-1104');\n else if (!value.joinKey && !value.joinKeystore)\n throw new DCPError('Compute group must contain a joinKey or a joinKeystore', 'DCPL-1105');\n }\n \n try\n {\n const cgPayload = await computeGroups.addJobToGroups(this.address, this.computeGroups);\n success = true; // To support older version of CG service where addJobToGroups had void/undefined return.\n if (cgPayload) success = cgPayload.success;\n debugging('dcp-client') && console.debug('job/index: addJobToGroups cgPayload:', cgPayload ? cgPayload : 'cgPayload is not defined; probably from legacy CG service.');\n }\n catch (e)\n {\n debugging('dcp-client') && console.debug('job/index: addJobToGroups threw exception:', e);\n success = false;\n }\n\n computeGroups.closeServiceConnection().catch((err) => {\n console.error('Warning: could not close compute groups service connection', err)\n });\n\n /* Could not put the job in any compute group, even though the user wanted it to run. Cancel the job. */\n if (!success)\n {\n await this.cancel('compute-groups::Unable to join any compute groups');\n throw new DCPError(`Access Denied::Failed to add job ${this.address} to any of the desired compute groups on ${this.scheduler}`, '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 /**\n * There is currently not way to access OAM instance on the job level.\n * This case is similar to the fetch case in result-handle.js.\n * The value to fetchURI is either a dataURI, RDS that user defined or scheduler disk location.\n */\n let result = await fetchURI(_result, false);\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 - 1);\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 this.#jobDeployRef = setInterval(() => undefined, 271828); /* Create a reference to the deploy cycle. */\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.marshaledArguments)\n submitPayload.marshaledArguments = this.marshaledArguments;\n else 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 this.jobArguments = this.jobArguments.map((e) => new URL(e));\n\n submitPayload.marshaledArguments = kvin.marshal(encodeJobValueList(this.jobArguments, 'jobArguments'));\n }\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 }\n debugging('dcp-client') && console.debug('submitPayload.marshaledArguments', JSON.stringify(submitPayload.marshaledArguments).length, JSON.stringify(submitPayload).length);\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 /* if connection is lost during useDeployConnection, error message that gets returned is not thrown, so throw here so that we can reattempt deployment */\n if (deployed instanceof Error && deployed.code === 'DCPC-1013'){\n throw deployed;\n }\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();\n computeGroups.closeServiceConnection().catch(handleErr);\n this.#jobDeployRef = clearInterval(this.#jobDeployRef);\n \n // Yes, it is possible for deployed or deployed.payload to be undefined.\n if (deployed && deployed.payload)\n {\n if (deployed.payload.code === 'ENOTFOUND')\n throw new DCPError(`Failed to submit job; account ${submitPayload.paymentAccount} does not have sufficient balance (${deployed.payload.info.deployCost}⊇ needed to deploy this job)`, deployed.payload); \n throw new DCPError('Failed to submit job:' + deployed.payload?.message, deployed.payload.code);\n }\n throw new DCPError(`Failed to submit job (no payload; ${deployed || typeof 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.address,\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(operation, payload)\n {\n if (!this.useDeployConnection.uses)\n this.useDeployConnection.uses = 0;\n this.useDeployConnection.uses++;\n if (!this.deployConnection)\n {\n debugging('deploy-connection') && console.info(`1453: making a new deployConnection...`)\n this.deployConnection = new protocolV4.Connection(dcpConfig.scheduler.services.jobSubmit); \n this.deployConnection.on('end', () => { this.deployConnection = null; });\n }\n if (this.deployConnectionTimeout)\n clearTimeout(this.deployConnectionTimeout);\n\n debugging('deploy-connection') && console.info(`1460: sending ${operation} request...`);\n const deployPromise = this.deployConnection.request(operation, payload);\n \n deployPromise.finally(() => {\n this.useDeployConnection.uses--;\n\n debugging('deploy-connection') && console.info(`1462: deployConnection done ${operation} request, connection uses is ${this.useDeployConnection.uses}`)\n\n this.deployConnectionTimeout = setTimeout(() => {\n if (this.useDeployConnection.uses === 0 && this.deployConnection)\n {\n debugging('deploy-connection') && console.info(`1469: done with deployConn, closing...`);\n // if we're done w/ the connection, then remove its cleanup\n // function, close it, and clean up manually to make room for a\n // new conn when needed (ie, don't leave the closing conn where\n // someone could accidentally pick it up)\n this.deployConnection.removeAllListeners('end');\n this.deployConnection.close();\n this.deployConnection = null;\n }\n }, (dcpConfig.job.deployCloseTimeout || 10 * 1000));\n if (this.deployConnectionTimeout.unref)\n this.deployConnectionTimeout.unref();\n }); \n \n return deployPromise;\n }\n \n /**\n * Close the connection to the job submit (if it exists), and clear the close timeout (if needed).\n */\n closeDeployConnection()\n {\n if (this.deployConnection)\n this.deployConnection.close();\n if (this.deployConnectionTimeout)\n clearTimeout(this.deployConnectionTimeout);\n }\n\n unref()\n {\n if (this.#jobRef)\n this.#jobRef = clearInterval(this.#jobRef);\n if (this.#jobDeployRef)\n this.#jobDeployRef = clearInterval(this.#jobDeployRef);\n }\n\n ref()\n {\n this.#jobRef = setInterval(() => undefined, 1618033988);\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 slice 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<URL|{string: string, method:string, MIMEType: string }>} - URL or object with 3 properties:\n * string - serialization of value\n * method - how value was serialized (e.g. 'kvin', 'json')\n * MIMEType - MIME information corresponding to method (e.g. 'application/kvin', 'application/json')\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 or RemoteDataPattern => apply new URI(?)\n * 2. an Array-like objects => apply serializeJobValue\n * Values sent to the scheduler in payload are either URL or the result of calling serializeJobValue.\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(serializeJobValue(value.href));\n else\n list.push(serializeJobValue(value));\n }\n\n return list;\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?");
|
|
4349
|
+
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// @ts-check\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 { serializeJobValue, 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\");\nconst { setTimeout } = __webpack_require__(/*! dcp/common/dcp-timers */ \"./src/common/dcp-timers.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/** @typedef {import('dcp/dcp-client/range-object').RangeLike} RangeLike */\n/** @typedef {import('dcp/dcp-client/wallet/keystore').Keystore} Keystore */\n/** @typedef {import('dcp/utils/jsdoc-types').SupervisorOptions} SupervisorOptions */\n/** @typedef {import('dcp/common/dcp-events').PropagatingEventEmitter} PropagatingEventEmitter */\n/** @typedef {import('scheduler-v4/libexec/job-submit/operations/submit').MarshaledInputData} MarshaledInputData */\n\n/**\n * Ensure input data is an appropriate format\n * @param {RangeObject | DistributionRange | RemoteDataSet | Array | RangeLike | Iterable}\n * inputData - A URI-shaped string, a [Multi]RangeObject-constructing value, or\n * an array of slice data\n * @return {RangeObject | DistributionRange | RemoteDataSet | Array | RangeLike | Iterable}\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 {PropagatingEventEmitter}\n * @hideconstructor\n * @access public\n */\nclass Job extends PropagatingEventEmitter\n{\n /* Private class fields */\n #jobDeployRef; /* Event loop reference to the job deploy cyle. Different from Job.ref() which will reference the job. */\n #jobRef; /* Keep the job alive. */\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\n /* A default reference the Job is set. */\n this.ref();\n\n if (typeof arguments[0] === 'function')\n arguments[0] = arguments[0].toString();\n\n if (typeof arguments[0] === 'string')\n {\n const { encodeDataURI } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\n this.workFunctionURI = encodeDataURI(arguments[0], 'application/javascript');\n } \n else if (DcpURL.isURL(arguments[0]))\n this.workFunctionURI = arguments[0].href;\n\n this.jobInputData = wrangleData(arguments[1] || []);\n this.jobArguments = wrangleData(arguments[2] || []);\n \n log('num wrangledInputData:', this.jobInputData.length);\n log('num wrangledArguments:', this.jobArguments.length);\n\n this.initEventSystems();\n\n /**\n * An object describing the cost the user believes each the average slice will incur, in terms of CPU/GPU and I/O.\n * If defined, this object is used to provide initial scheduling hints and to calculate escrow amounts.\n *\n * @type {object}\n * @access public\n */\n this.initialSliceProfile = undefined;\n \n // The max value that the client is willing to spend to deploy\n // (list on the scheduler, doesn't include compute payment)\n // maxDeployPayment is the max the user is willing to pay to DCP (as a\n // Hold), in addition to the per-slice offer and associated scrape.\n // Currently calculated as `deployCost = costPerKB *\n // (JSON.stringify(job).length / 1024) // 1e-9 per kb`\n // @todo: figure this out / er nov 2018\n this.maxDeployPayment = 1;\n\n /**\n * An object describing the requirements that workers must have to be eligible for this job. The default values are set in the job-submitter, and only the client specified\n * requirements are sent over the wire. See {@link https://docs.dcp.dev/specs/compute-api.html#requirements-objects|Requirements Objects}.\n *\n * @type {object}\n * @access public\n */\n this.requirements = {};\n\n /**\n * A place to store public-facing attributes of the job. Anything stored on this object will be available inside the work \n * function (see {@link module:dcp/compute~sandboxEnv.work}). The properties documented here may be used by workers to display what jobs are currently being \n * worked on.\n * @access public\n * @property {string} name Public-facing name of this job.\n * @property {string} description Public-facing description for this job.\n * @property {string} link Public-facing link to external resource about this job.\n */\n this.public = {\n name: null,\n description: null,\n link: null,\n };\n \n /**\n * A cryptographic receipt indicating deployment of the job on the scheduler\n * @type {object}\n * @access public\n */\n this.receipt = null;\n \n /**\n * a SliceProfile object which contains the average costs for the slices which have been computed to date.\n * Until the first result is returned, this property is undefined.\n * @type {object}\n * @access public\n */\n this.meanSliceProfile = null;\n \n /**\n * A number (can be null, undefined, or infinity) describing the estimationSlicesRemaining in the jpd (dcp-2593)\n * @type {number}\n * @access public\n */\n this.estimationSlices = undefined;\n /**\n * When true, allows a job in estimation to have requestTask return multiple estimation slices.\n * This flag applies independent of infinite estimation, viz., this.estimationSlices === null .\n * @type {boolean}\n * @access public\n */\n this.greedyEstimation = false;\n /**\n * tunable parameters per job\n * @access public\n * @param {object} tuning \n * @param {string} tuning.kvin Encode the TypedArray into a string, trying multiple methods to determine optimum \n * size/performance. The this.tune variable affects the behavior of this code this:\n * @param {boolean} speed If true, only do naive encoding: floats get represented as byte-per-digit strings\n * @param {boolean} size If true, try the naive, ab8, and ab16 encodings; pick the smallest\n * If both are false try the naive encoding if under typedArrayPackThreshold and use if smaller\n * than ab8; otherwise, use ab8\n */\n this.tuning = {\n kvin: {\n size: false,\n speed: false,\n },\n }\n /* For API interface to end-users only */\n Object.defineProperty(this, 'id', {\n get: () => this.address,\n set: (id) => { this.address = id }\n });\n\n this.uuid = uuidv4(); /** @see {@link https://kingsds.atlassian.net/browse/DCP-1475?atlOrigin=eyJpIjoiNzg3NmEzOWE0OWI4NGZkNmI5NjU0MWNmZGY2OTYzZDUiLCJwIjoiaiJ9|Jira Issue} */\n this.dependencies = []; /* dependencies of the work function */\n this.requirePath = []; /* require path for dependencies */\n this.modulePath = []; /* path to module that invoked .exec() for job */\n this.connected = false; /* true when exec or resume called */\n this.results = new ResultHandle(this); /* result handle */\n this.collateResults = true; /* option to receive results as they are computed & ensure all are received on finish */\n this.contextId = null; /* optional string which is used to indicate to caching mechanisms different keystores with same name */ \n this.force100pctCPUDensity = false; /* tell scheduler to assume this job uses 100% cpu density */\n this.workerConsole = false; /* tell workers to log more information about failures in the evaluator this job causes */\n this.address = null; /* job address, created by scheduler during exec call. */\n this.paymentAccountKeystore = null; /* keystore for payment for job to come from */\n this.status = { /* job status details */\n runStatus: null,\n total: null,\n distributed: null,\n computed: null\n };\n\n // service locations\n this.scheduler = dcpConfig.scheduler.services.jobSubmit.location;\n this.bank = dcpConfig.bank.services.bankTeller.location;\n\n // Compute groups. Add to public compute group by default\n this.computeGroups = [ Object.assign({}, schedulerConstants.computeGroups.public) ];\n\n // A Worker object that describes the local worker if one is used for a job.\n this.localWorker = {\n /** @type {object[]} */\n origins: [],\n originManager: {\n /** \n * @param {string} origin\n * @param {string | null} purpose \n * @param {string | null} key\n */\n add: (origin, purpose, key = null) => {\n this.localWorker.origins.push({\n origin: origin,\n purpose: purpose,\n key: key,\n });\n },\n },\n };\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 // Careful -- this.schedulerConnection does not exist.\n const response = await this.schedulerConnection.request('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.request('embiggenFeeStructure', {\n feeStructureAddress: this[INTERNAL_SYMBOL].payloadDetails.feeStructureId,\n additionalEscrow: new BigNumber(fundsRequired),\n fromAddress: this.paymentAccountKeystore.address,\n }, this.paymentAccountKeystore);\n\n bankConnection.close();\n const escrowReceipt = response.payload;\n return escrowReceipt;\n }\n\n /**\n * create bundles for local dependencies\n */\n _pack ()\n {\n var retval = (__webpack_require__(/*! ./node-modules */ \"./src/dcp-client/job/node-modules.js\").createModuleBundle)(this.dependencies);\n return retval;\n }\n\n /** \n * Collect all of the dependencies together, throw them into a BravoJS\n * module which sideloads them as a side effect of declaration, and transmit\n * them to the package manager. Then we return the package descriptor object,\n * which is guaranteed to have only one file in it.\n *\n * @returns {object} with properties name and files[0]\n */\n async _publishLocalModules()\n {\n const dcpPublish = __webpack_require__(/*! dcp/common/dcp-publish */ \"./src/common/dcp-publish.js\");\n \n const { tempFile, hash, unresolved } = await this._pack();\n\n if (!tempFile) {\n return { unresolved };\n }\n\n const sideloaderFilename = tempFile.filename;\n const pkg = {\n name: `dcp-pkg-v1-localhost-${hash.toString('hex')}`,\n version: '1.0.0',\n files: {\n [sideloaderFilename]: `${sideloaderModuleIdentifier}.js`,\n },\n }\n\n await dcpPublish.publish(pkg);\n tempFile.remove();\n\n return { pkg, unresolved };\n }\n \n /**\n * This function specifies a module dependency (when the argument is a string)\n * or a list of dependencies (when the argument is an array) of the work\n * function. This function can be invoked multiple times before deployment.\n * @param {string | string[]} modulePaths - A string or array describing one\n * or more dependencies of the job.\n * @access public\n */\n requires (modulePaths)\n {\n if (typeof modulePaths !== 'string' && (!Array.isArray(modulePaths) || modulePaths.some((modulePath) => typeof modulePath !== 'string')))\n throw new TypeError('The argument to dependencies is not a string or an array of strings');\n else if (modulePaths.length === 0)\n throw new RangeError('The argument to dependencies cannot be an empty string or array');\n else if (Array.isArray(modulePaths) && modulePaths.some((modulePath) => modulePath.length === 0))\n throw new RangeError('The argument to dependencies cannot be an array containing an empty string');\n\n if (!Array.isArray(modulePaths))\n modulePaths = [modulePaths];\n\n for (const modulePath of modulePaths)\n {\n if (modulePath[0] !== '.' && modulePath.indexOf('/') !== -1)\n {\n const modulePrefixRegEx = /^(.*)\\/.*?$/;\n const [, modulePrefix] = modulePath.match(modulePrefixRegEx);\n if (modulePrefix && this.requirePath.indexOf(modulePrefix) === -1)\n this.requirePath.push(modulePrefix);\n }\n this.dependencies.push(modulePath);\n }\n }\n \n /** Set the account upon which funds will be drawn to pay for the job.\n * @param {module:dcp/wallet.BankAccountKeystore} 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('Argument must be an instance of Keystore');\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 this.resultStorageDetails = new URL(locationUrl);\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('postParams must be an object');\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 this.localExecAllowedOrigins = {\n any: [],\n fetchData: [],\n fetchWorkFunctions: [],\n fetchArguments: [],\n sendResults: [],\n };\n\n let worker;\n this.on('accepted', () => {\n // Fill in localexec-specific requirements to config\n /** @type {SupervisorOptions} */\n const config = {\n trustComputeGroupOrigins: false,\n allowOrigins: this.localExecAllowedOrigins,\n paymentAddress: this.identityKeystore.address,\n allowConsoleAccess: true,\n jobAddresses: [this.address],\n cores: { cpu: cores, gpu: undefined },\n sandboxOptions: {\n ignoreNoProgress: true,\n SandboxConstructor: (DCP_ENV.platform === 'nodejs' && (__webpack_require__(/*! ../worker/evaluators */ \"./src/dcp-client/worker/evaluators/index.js\").nodeEvaluatorFactory)()),\n },\n };\n\n if (this.localWorker?.origins?.length)\n {\n for (let i = 0; i < this.localWorker.origins.length; i++) \n {\n const originToAdd = this.localWorker.origins[i];\n\n /* Push origins to localExecAllowedOrigins. If a null purpose is provided, \n push it onto the 'any' array of this.localExecAllowedOrigins */\n this.localExecAllowedOrigins[(originToAdd.purpose === null ? 'any' : originToAdd.purpose)].push(originToAdd.origin);\n }\n }\n\n config.cores = Worker.defaultCores(config);\n worker = new Worker(this.identityKeystore, config);\n\n // @ts-ignore\n this.localWorker = worker;\n\n worker.on('error', (error) => {\n console.error('LocalExec Error:', error);\n });\n\n worker.on('warning', (warning) => {\n console.warn('LocalExec Warning:', warning);\n });\n\n worker.start().catch((e) => {\n console.error('Failed to start worker for localExec:');\n console.error(e.message);\n });\n });\n\n if (DCP_ENV.platform === 'nodejs')\n {\n // Determine type of input data\n // marshalInputData actually returns the union of dataRange, dataValues, dataPattern, sliceCount,\n // but there isn't a simple way to express that in Javascript.\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 inputSetURIs.push(dataValues[i]);\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 // 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 {\n // Coerce into a URI to handle OS specific path separators.\n const inputFileURI = new URL(`file://${inputSetFiles[i].filename}`);\n this.localExecAllowedOrigins['fetchData'].push(inputFileURI.pathname);\n }\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 // data:URI - dont care about origins\n const workFunction = await fetchURI(this.workFunctionURI, false);\n workFunctionFile.writeSync(workFunction);\n \n const workFunctionFileURL = new URL('file://' + workFunctionFile);\n this.workFunctionURI = workFunctionFileURL.href;\n this.localExecAllowedOrigins['fetchWorkFunctions'].push(workFunctionFileURL.pathname);\n }\n \n let encodedJobArgumentUris = [];\n if (this.jobArguments)\n {\n if (this.jobArguments instanceof RemoteDataPattern) /* Not supported */\n throw new DCPError('Cannot use RemoteDataPattern as work function arguments', 'EBADARG')\n if (this.jobArguments instanceof RemoteDataSet) /* Entire set is RemoteDataSet */\n {\n this.jobArguments.forEach((e) =>\n {\n encodedJobArgumentUris.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 encodedJobArgumentUris.push(this.jobArguments[i]);\n else\n {\n if (this.jobArguments[i] instanceof RemoteDataSet) /* Member of set is RemoteDataSet */\n {\n this.jobArguments[i].forEach((e) =>\n {\n encodedJobArgumentUris.push(new URL(e));\n });\n }\n else /* Actual Value */\n {\n const localArgFile = createTempFile(`dcp-localExec-argument-${i}-XXXXXXXXX`, 'kvin');\n const localArgFileURI = new URL(`file://${localArgFile.filename}`);\n\n localArgFile.writeSync(JSON.stringify(kvinMarshal(this.jobArguments[i])));\n this.localExecAllowedOrigins['fetchArguments'].push(localArgFileURI.pathname);\n encodedJobArgumentUris.push(localArgFileURI);\n }\n }\n } \n }\n }\n this.marshaledArguments = kvinMarshal(encodedJobArgumentUris);\n }\n \n return this.exec(...args).finally(() => {\n if (worker) {\n const timerHandle = setTimeout(() => {\n // stop the worker\n worker.stop(true);\n }, 3000);\n // Allow workers and localExec to exit.\n if (timerHandle.unref)\n timerHandle.unref();\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\n if (this.connected)\n throw new Error(`Exec called twice on the same job handle (${this.address})`);\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'}.<br><br> 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 let msg = `Upload a DCP bank account keystore<br> containing compute credits.`;\n try\n {\n ks = await wallet.get({ contextId: this.contextId, jobName: this.public.name, msg});\n }\n catch (e)\n {\n if (e.code !== ClientModal.CancelErrorCode) throw e;\n };\n if (ks)\n {\n try\n {\n await ks.unlock(undefined, parseFloat(dcpConfig.job.maxDeployTime));\n locked = false;\n }\n catch (e)\n {\n // prompt user again if user enters password incorrectly, exit modal otherwise\n if (e.code !== wallet.unlockFailErrorCode) throw e;\n }\n }\n if (safety++ > 1000) throw new Error('EINFINITY: job.exec tried wallet.get more than 1000 times.')\n } while (locked);\n this.setPaymentAccountKeystore(ks)\n }\n \n // We either have a valid keystore + password or we have rejected by this point.\n if (!this.slicePaymentOffer)\n throw new Error('A payment profile must be assigned before executing the job');\n else\n this.feeStructure = this.slicePaymentOffer.toFeeStructure(this.jobInputData.length);\n\n if (!this.address)\n {\n try\n {\n this.readyStateChange('init');\n await this.deployJob();\n const listenersPromise = this.addInitialEvents();\n const computeGroupsPromise = this.joinComputeGroups();\n let uploadSlicePromise;\n // if job data is by value then upload data to the scheduler in a staggered fashion\n if (Array.isArray(this.dataValues) && !this.marshaledDataValues)\n {\n this.readyStateChange('uploading');\n uploadSlicePromise = addSlices(this.dataValues, this.address, tunedKvin)\n .then(() => {\n debugging('slice-upload') && console.info(`970: slice data uploaded, closing job...`);\n return this.close();\n });\n }\n \n // await all promises for operations that can be done after the job is deployed\n await Promise.all([listenersPromise, computeGroupsPromise, uploadSlicePromise]);\n \n this.readyStateChange('deployed');\n this.emit('accepted', this);\n this.#jobDeployRef = clearInterval(this.#jobDeployRef); /* Remove the reference to job deploy. */\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 this.startEventBandaidWatchdog();\n \n const onComplete = () => {\n this.stopEventBandaidWatchdog();\n resolve(this.results); \n };\n\n const onCancel = (event) => {\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 this.stopEventBandaidWatchdog();\n\n reject(new DCPError(errorMsg, event.code));\n };\n\n const onNoFunds = (event) => {\n let errorMsg = `Job paused due to ENOFUNDS. ${event.remainingSlices} remaining slices at ${event.slicePaymentAmount} DCCs per slice.`;\n let errorCode = 'ENOFUNDS';\n\n this.stopEventBandaidWatchdog();\n\n reject(new DCPError(errorMsg, errorCode));\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 case jobStatus.paused:\n onNoFunds(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 this.eventSubscriber.close();\n await computeGroups.closeServiceConnection().catch((err) => {\n console.error('Warning: could not close compute groups service connection', err);\n });\n })();\n\n /* Remove the reference to the Job. */\n this.unref();\n });\n }\n\n /**\n * Start the event-bandaid watchdog. Every [configurable] seconds, fetch\n * the job report and do appropriate things if the job is stopped.\n */\n startEventBandaidWatchdog()\n {\n if (this.eventBandaidHandle)\n this.stopEventBandaidWatchdog();\n\n this.eventBandaidHandle = setInterval(() => {\n this.eventBandaid();\n }, 60 * 1000);\n\n // if we are in a nodelike environment with unreffable timers, unref the\n // timer so it doesn't hold the process open. \n // \n // (n.b. while I understand that we _want_ the process to stay open while\n // the job lives, but this is Not The Way; the whole eventBandaid is a\n // temporary hack which will go away in time ~ER 2022-11-02)\n if (this.eventBandaidHandle.unref)\n this.eventBandaidHandle.unref();\n }\n\n /**\n * Stop and clean up the even-bandaid watchdog.\n */\n stopEventBandaidWatchdog()\n {\n if (this.eventBandaidHandle)\n clearInterval(this.eventBandaidHandle);\n\n this.eventBandaidHandle = false;\n }\n\n /**\n * Called on an interval, started by .exec and stopped by\n * onComplete/onCancel. Fetches the jobReport and verifies the job is\n * not in a terminal state; if the job is finished or cancelled, we\n * emit a synthetic internal stop event to trigger regular cleanup\n * (result retrieval, etc) and finalize the job handle.\n */\n eventBandaid()\n {\n this.getJobInfo()\n .then(jobInfo => {\n if ([jobStatus.finished, jobStatus.cancelled].includes(jobInfo.status))\n this.ee.emit('stopped', { runStatus: jobInfo.status });\n })\n .catch(error => {\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 this.on('nofunds', (ev) => {\n this.ee.emit('stopped', ev);\n })\n\n // Connect listeners that were set up before exec\n if (this.desiredEvents.includes('result'))\n this.listeningForResults = true;\n await this.subscribeNewEvents(this.desiredEvents);\n\n // Connect listeners that are set up after exec\n this.on('newListener', (evt) => {\n if (evt === 'newListener' || this.desiredEvents.includes(evt))\n return;\n this.subscribeNewEvents([evt]);\n });\n \n // automatically add a listener for results if collateResults is on\n if (this.collateResults && !this.listeningForResults)\n this.on('result', () => {});\n\n debugging('dcp-client') && console.debug('subscribedEvents', this.desiredEvents);\n\n // If we have listeners for job.work, subscribe to custom events\n if (this.listenForCustomEvents)\n await this.subscribeCustomEvents();\n // Connect work event listeners that are set up after exec\n else\n this.work.on('newListener', () => this.subscribeCustomEvents());\n }\n \n /**\n * Subscribes to either reliable events or optional events. It is assumed that\n * any call to this function will include only new events.\n * @param {string[]} events \n */\n async subscribeNewEvents (events)\n {\n const reliableEvents = [];\n const optionalEvents = [];\n for (let eventName of events)\n {\n eventName = eventName.toLowerCase();\n if (this.eventTypes[eventName] && this.eventTypes[eventName].reliable)\n reliableEvents.push(eventName);\n else if (this.eventTypes[eventName] && !this.eventTypes[eventName].reliable)\n optionalEvents.push(eventName);\n else\n debugging('dcp-client') && console.debug(`Job handler has listener ${eventName} which isn't an event-router event.`);\n }\n if (debugging('dcp-client'))\n {\n console.debug('reliableEvents', reliableEvents);\n console.debug('optionalEvents', optionalEvents);\n }\n await this.eventSubscriber.subscribeManyEvents(reliableEvents, optionalEvents, { filter: { job: this.address } });\n }\n \n /**\n * Establishes listeners for worker events when requested by the client\n */\n async subscribeCustomEvents ()\n {\n if (!this.listeningForCustomEvents)\n await this.eventSubscriber.subscribeManyEvents([], ['custom'], { filter: { job: this.address } });\n this.listeningForCustomEvents = true\n }\n \n async joinComputeGroups ()\n {\n // localExec jobs are not entered in any compute group.\n if (!this.inLocalExec && this.computeGroups && this.computeGroups.length > 0)\n {\n this.readyStateChange('compute-groups');\n computeGroups.addRef(); // Just in case we're doing a Promise.all on multiple execs.\n\n // Add this job to its currently-defined compute groups (as well as public group, if included)\n let success;\n \n if (!Array.isArray(this.computeGroups)) \n throw new DCPError('Compute groups must be wrapped in an Array', 'DCPL-1101');\n\n for (let i = 0; i < this.computeGroups.length; i++)\n {\n let value = this.computeGroups[i];\n \n if (typeof value !== 'object')\n throw new DCPError(`This compute group: ${value[i]} must be an object`, 'DCPL-1102');\n \n if (value.joinKey && typeof value.joinKey !== 'string' && !(value.joinKey instanceof String))\n throw new DCPError(`This join key: ${value.joinKey} must be a string or a string literal`, 'DCPL-1103');\n else if (value.joinKeystore && !(value.joinKeystore instanceof wallet.Keystore))\n throw new DCPError(`This join Keystore: ${value.joinKeystore} must be an instance of wallet.Keystore`, 'DCPL-1104');\n else if (!value.joinKey && !value.joinKeystore)\n throw new DCPError('Compute group must contain a joinKey or a joinKeystore', 'DCPL-1105');\n }\n \n try\n {\n const cgPayload = await computeGroups.addJobToGroups(this.address, this.computeGroups);\n success = true; // To support older version of CG service where addJobToGroups had void/undefined return.\n if (cgPayload) success = cgPayload.success;\n debugging('dcp-client') && console.debug('job/index: addJobToGroups cgPayload:', cgPayload ? cgPayload : 'cgPayload is not defined; probably from legacy CG service.');\n }\n catch (e)\n {\n debugging('dcp-client') && console.debug('job/index: addJobToGroups threw exception:', e);\n success = false;\n }\n\n computeGroups.closeServiceConnection().catch((err) => {\n console.error('Warning: could not close compute groups service connection', err)\n });\n\n /* Could not put the job in any compute group, even though the user wanted it to run. Cancel the job. */\n if (!success)\n {\n await this.cancel('compute-groups::Unable to join any compute groups');\n throw new DCPError(`Access Denied::Failed to add job ${this.address} to any of the desired compute groups on ${this.scheduler}`, '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 /**\n * There is currently not way to access OAM instance on the job level.\n * This case is similar to the fetch case in result-handle.js.\n * The value to fetchURI is either a dataURI, RDS that user defined or scheduler disk location.\n */\n let result = await fetchURI(_result, false);\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 - 1);\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 this.#jobDeployRef = setInterval(() => undefined, 271828); /* Create a reference to the deploy cycle. */\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.marshaledArguments)\n submitPayload.marshaledArguments = this.marshaledArguments;\n else 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 this.jobArguments = this.jobArguments.map((e) => new URL(e));\n\n submitPayload.marshaledArguments = kvin.marshal(encodeJobValueList(this.jobArguments, 'jobArguments'));\n }\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 }\n debugging('dcp-client') && console.debug('submitPayload.marshaledArguments', JSON.stringify(submitPayload.marshaledArguments).length, JSON.stringify(submitPayload).length);\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 /* if connection is lost during useDeployConnection, error message that gets returned is not thrown, so throw here so that we can reattempt deployment */\n if (deployed instanceof Error && deployed.code === 'DCPC-1013'){\n throw deployed;\n }\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();\n computeGroups.closeServiceConnection().catch(handleErr);\n this.#jobDeployRef = clearInterval(this.#jobDeployRef);\n \n // Yes, it is possible for deployed or deployed.payload to be undefined.\n if (deployed && deployed.payload)\n {\n if (deployed.payload.code === 'ENOTFOUND')\n throw new DCPError(`Failed to submit job; account ${submitPayload.paymentAccount} does not have sufficient balance (${deployed.payload.info.deployCost}⊇ needed to deploy this job)`, deployed.payload); \n throw new DCPError('Failed to submit job:' + deployed.payload?.message, deployed.payload.code);\n }\n throw new DCPError(`Failed to submit job (no payload; ${deployed || typeof 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.address,\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(operation, payload)\n {\n if (!this.useDeployConnection.uses)\n this.useDeployConnection.uses = 0;\n this.useDeployConnection.uses++;\n if (!this.deployConnection)\n {\n debugging('deploy-connection') && console.info(`1453: making a new deployConnection...`)\n this.deployConnection = new protocolV4.Connection(dcpConfig.scheduler.services.jobSubmit); \n this.deployConnection.on('end', () => { this.deployConnection = null; });\n }\n if (this.deployConnectionTimeout)\n clearTimeout(this.deployConnectionTimeout);\n\n debugging('deploy-connection') && console.info(`1460: sending ${operation} request...`);\n const deployPromise = this.deployConnection.request(operation, payload);\n \n deployPromise.finally(() => {\n this.useDeployConnection.uses--;\n\n debugging('deploy-connection') && console.info(`1462: deployConnection done ${operation} request, connection uses is ${this.useDeployConnection.uses}`)\n\n this.deployConnectionTimeout = setTimeout(() => {\n if (this.useDeployConnection.uses === 0 && this.deployConnection)\n {\n debugging('deploy-connection') && console.info(`1469: done with deployConn, closing...`);\n // if we're done w/ the connection, then remove its cleanup\n // function, close it, and clean up manually to make room for a\n // new conn when needed (ie, don't leave the closing conn where\n // someone could accidentally pick it up)\n this.deployConnection.removeAllListeners('end');\n this.deployConnection.close();\n this.deployConnection = null;\n }\n }, (dcpConfig.job.deployCloseTimeout || 10 * 1000));\n if (this.deployConnectionTimeout.unref)\n this.deployConnectionTimeout.unref();\n }); \n \n return deployPromise;\n }\n \n /**\n * Close the connection to the job submit (if it exists), and clear the close timeout (if needed).\n */\n closeDeployConnection()\n {\n if (this.deployConnection)\n this.deployConnection.close();\n if (this.deployConnectionTimeout)\n clearTimeout(this.deployConnectionTimeout);\n }\n\n unref()\n {\n if (this.#jobRef)\n this.#jobRef = clearInterval(this.#jobRef);\n if (this.#jobDeployRef)\n this.#jobDeployRef = clearInterval(this.#jobDeployRef);\n }\n\n ref()\n {\n this.#jobRef = setInterval(() => undefined, 1618033988);\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 slice 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<URL|{string: string, method:string, MIMEType: string }>} - URL or object with 3 properties:\n * string - serialization of value\n * method - how value was serialized (e.g. 'kvin', 'json')\n * MIMEType - MIME information corresponding to method (e.g. 'application/kvin', 'application/json')\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 or RemoteDataPattern => apply new URI(?)\n * 2. an Array-like objects => apply serializeJobValue\n * Values sent to the scheduler in payload are either URL or the result of calling serializeJobValue.\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(serializeJobValue(value.href));\n else\n list.push(serializeJobValue(value));\n }\n\n return list;\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?");
|
|
4350
4350
|
|
|
4351
4351
|
/***/ }),
|
|
4352
4352
|
|
|
@@ -4447,7 +4447,7 @@ eval("/* provided dependency */ var process = __webpack_require__(/*! ./node_mod
|
|
|
4447
4447
|
\*************************************************/
|
|
4448
4448
|
/***/ ((module, __unused_webpack_exports, __webpack_require__) => {
|
|
4449
4449
|
|
|
4450
|
-
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\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 \n window.alert('DCP Worker Announcement: ' + message);\n\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=
|
|
4450
|
+
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\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 \n window.alert('DCP Worker Announcement: ' + message);\n\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=8a95bd250c03421b473f543c2e9c7a85516d622c,' + 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?");
|
|
4451
4451
|
|
|
4452
4452
|
/***/ }),
|
|
4453
4453
|
|
|
@@ -4604,7 +4604,7 @@ eval("/**\n * @file node-localExec.js Node-specific support for cre
|
|
|
4604
4604
|
/***/ ((__unused_webpack_module, exports, __webpack_require__) => {
|
|
4605
4605
|
|
|
4606
4606
|
"use strict";
|
|
4607
|
-
eval("/**\n * @file worker/index.js This module implements the Worker API, used to create workers for \n * earning DCCs. \n *\n * To use Supervisor2 set the environment variable `USE_SUPERVISOR2` or \n * set dcpConfig.rollout.supervisor=2.\n *\n * @author Ryan Rossiter <ryan@kingsds.network>\n * Paul <paul@distributive.network>\n * @date May 2020\n * June, July 2022, Feb-April 2023\n * \n * @module dcp/worker\n * @access public\n * @copyright Copyright (c) 2018-2023, Distributive Corp. All Rights Reserved\n */\n// @ts-check\n\n\nconst debugging = (__webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope)('worker');\nconst { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\nconst { SchedMsg } = __webpack_require__(/*! dcp/dcp-client/schedmsg */ \"./src/dcp-client/schedmsg/index.js\");\nconst EventEmitter = __webpack_require__(/*! events */ \"./node_modules/events/events.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 { Address } = __webpack_require__(/*! dcp/dcp-client/wallet/eth */ \"./src/dcp-client/wallet/eth.js\");\nconst { Keystore } = __webpack_require__(/*! dcp/dcp-client/wallet/keystore */ \"./src/dcp-client/wallet/keystore.js\");\nconst dcpEnv = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\nconst dcp_timers = __webpack_require__(/*! dcp/common/dcp-timers */ \"./src/common/dcp-timers.js\");\nconst DISABLE_WORKER_CACHE_KEY = 'disable_worker';\nvar Supervisor; /* Supervisor ctor - this will die when we kill sup1 */\n\n// Supervisor2 typedefs\n/** @typedef {import('./supervisor2/slice2').Slice} Slice */\n/** @typedef {import('./supervisor2/sandbox2').Sandbox} Sandbox */\n/** @typedef {import('./supervisor2/options').Options} Options */\n\n// Supervisor1 typedefs\n// /** @typedef {import('./sandbox').Sandbox} Sandbox */\n// /** @typedef {import('./slice').Slice} Slice */\n\n/** @typedef {string} opaqueId */ // 22 character base64 string\n/** @typedef {import('dcp/dcp-client/wallet/eth').Address} Address */\n/** @typedef {import('dcp/dcp-client/wallet/keystore').Keystore} Keystore */\n/** @typedef {import('dcp/utils/jsdoc-types').SandboxOptions} SandboxOptions */\n/** @typedef {import('dcp/utils/jsdoc-types').SupervisorOptions} SupervisorOptions */\n/** @typedef {import('dcp/dcp-client/worker/origin-access-manager').OriginAccessManager} OriginAccessManager */\n\nfunction disableWorker ()\n{\n localStorage.setItem(DISABLE_WORKER_CACHE_KEY, true);\n}\n\n/** XXXwg remove this when we kill sup 1 */\nfunction determineSupervisorType()\n{\n if (Supervisor)\n return;\n\n if ( false\n || (__webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\").getenv)('USE_SUPERVISOR2')\n || (Number(dcpConfig.rollout?.supervisor) === 2))\n Supervisor = (__webpack_require__(/*! ./supervisor2 */ \"./src/dcp-client/worker/supervisor2/index.js\").Supervisor);\n else\n Supervisor = (__webpack_require__(/*! ./supervisor */ \"./src/dcp-client/worker/supervisor.js\").Supervisor);\n}\n\n/**\n * Fired when the worker begins fetching slices from the scheduler.\n * @access public\n */\nclass Worker extends EventEmitter\n{\n /** Private class fields */\n #supervisor;\n #hasRef; /* event loop reference; undefined=>auto, false=>never, timer=>holding */\n\n /**\n * Returns a new Worker instance.\n * This constructor may alter the workerOptions object, for version-based polyfilling and/or providing\n * key API properties, such as the cores object.\n *\n * @access public\n * @param {Keystore} identity - identity of the Worker; used for signing messages to\n * task distributor etc.\n * @param {SupervisorOptions} workerOptions - options specified for the worker; may be mutated\n */\n constructor(identity, workerOptions)\n {\n super({ captureRejections: false });\n\n if (typeof workerOptions !== 'object')\n throw new Error('workerOptions argument must be object; is ' + typeof workerOptions);\n if (typeof identity !== 'object' || !(identity instanceof Keystore))\n throw new Error(`identity argument must be Keystore; is ${typeof identity === 'object' ? identity.constructor.name : typeof identity}`);\n\n determineSupervisorType();\n\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 * It is important that dynamic changes in workerOptions are reflected in Supervisor operations. In the case where\n * cores are derived solely from defaultCoreDensity, defaultCores provides an object which stays up to date with\n * defaultCoreDensity changes until a value is explicitly set.\n *\n * @type {SupervisorOptions}\n * @access public\n */\n this.workerOptions = workerOptions;\n if (!this.workerOptions.hasOwnProperty('cores') || !this.workerOptions.cores.hasOwnProperty('cpu'))\n this.workerOptions.cores = Worker.defaultCores(this.workerOptions);\n else\n Worker.defaultInitialization(this.workerOptions);\n if (this.workerOptions.hasOwnProperty('maxWorkingSandboxes') && this.workerOptions.maxWorkingSandboxes !== undefined)\n Worker.handleMaxWorkingSandboxes(this.workerOptions); /** XXXwg deprecate this API once all apps use sup2 conf */\n\n /* Correct cores object is now plumbed in; patently-absurd configs are changed to have 0 cores so users can debug */\n if (!(this.workerOptions.cores.cpu >= 0))\n this.workerOptions.cores.cpu = 0;\n if (!(this.workerOptions.cores.gpu >= 0))\n this.workerOptions.cores.gpu = 0;\n\n /**\n * @todo XXXpfr Rip out the sup2/sup1 special-casing when we finally kill sup1.\n * @type {Supervisor}\n * @access public\n */\n this.#supervisor = new Supervisor(this, identity, this.workerOptions);\n\n debugging() && console.debug(`Worker options(isSup2:${this.#supervisor.isSupervisor2}):`, this.workerOptions.cores.cpu, this.workerOptions.cores.gpu, this.workerOptions);\n }\n\n get supervisorVersion() { return this.#supervisor.version };\n\n /**\n * Disables worker instances from being started. The user will need to manually intervene to re-enable workers.\n * @access public\n */\n static disableWorker ()\n {\n disableWorker();\n }\n\n /**\n * Starts the worker.\n * @access public\n */\n async start()\n {\n debugging() && console.debug('Starting worker');\n if (this.working)\n {\n debugging() && console.debug('Worker already working, ignoring start request');\n return;\n }\n\n if (localStorage.getItem(DISABLE_WORKER_CACHE_KEY))\n {\n await confirmPrompt(`Worker has been disabled by the DCP Security Team; check the @DCP_Protocol Twitter feed for more information before continuing.`)\n if (await confirmPrompt('Are you sure you would like to restart the worker?'))\n {\n localStorage.removeItem(DISABLE_WORKER_CACHE_KEY);\n console.log(\"Starting worker...\");\n }\n else\n return;\n }\n \n try\n {\n let dontStart = false;\n\n this.emit('beforeStart', function cancel() { dontStart = true });\n if (dontStart)\n return;\n\n this.#supervisor.work();\n this.working = true;\n this.schedMsg.start();\n if (this.#hasRef !== false)\n this.ref();\n this.emit('start');\n }\n catch(error)\n {\n debugging() && console.error(error);\n this.emit('error', error);\n this.stop(true);\n }\n }\n\n /**\n * Stops the worker.\n * The reason for this.#hasRef === false state is for\n * await start\n * stop <-- #end isn't called until after the 2nd start\n * start\n * <-- #end is called here because #supervisor.stopWork is placed on the microtask queue.\n * @access public\n * @param {boolean} [forceTerminate=false] Whether the worker should stop imediately or allow the current slices to finish.\n */\n async stop(forceTerminate = false)\n {\n if (!this.working) \n {\n debugging() && console.debug('Worker already stopping, ignoring stop request');\n return;\n }\n this.working = false;\n\n try \n {\n // We're using synchronous event emitters, so if the abort function is called immediately,\n // then #supervisor.stopWork(forceTerminate) will be called with forceTerminate === true.\n this.emit('stop', function abort() { forceTerminate = true });\n\n debugging('shutdown') && console.debug(`Worker.stop(${forceTerminate}): stopping schedMsg...`);\n await this.schedMsg.stop();\n debugging('shutdown') && console.debug(`Worker.stop(${forceTerminate}): schedmsg stopped ok; stopping supervisor...`);\n\n return this.#supervisor.stopWork(forceTerminate).finally(() => {\n debugging('shutdown') && console.debug(`Worker.stop(${forceTerminate}): supervisor stopped ok`);\n this.#end();\n });\n }\n catch (error) \n {\n debugging() && console.error(error);\n this.emit('error', error);\n }\n }\n\n #end()\n {\n if (this.#hasRef !== false)\n {\n this.unref();\n this.ref();\n }\n this.emit('end');\n }\n \n /**\n * Set the worker's event loop reference to \"auto\" - the default behaviour - which holds a reference as\n * long as we are working.\n */\n ref()\n {\n if (this.#hasRef === false)\n this.#hasRef = undefined;\n if (this.#hasRef === undefined && this.working)\n {\n this.#hasRef = dcp_timers.setInterval(function workerRef(){}, 314159);\n debugging('refs') && console.debug('worker - making event-loop reference');\n }\n }\n\n /**\n * Tell the worker to not hold an event loop reference, whether it is working or not.\n */\n unref()\n {\n if (this.#hasRef)\n {\n dcp_timers.clearInterval(this.#hasRef);\n debugging('refs') && console.debug('worker - clearing event-loop reference');\n }\n this.#hasRef = false;\n }\n\n /**\n * We have been handed a v1-style configuration object, so we use it to generate v2-style info. Then\n * we add a getter/setter on maxWorkingSandboxes so that it always reflects cores.cpu, and so that its\n * mutations (from the worker-app) update cores.cpu.\n *\n * @deprecated\n * Get rid of this when all the workers are updated so that worker options do not have property maxWorkingSandboxes.\n * Please use options.defaultCoreDensity or options.cores instead.\n * @param {SupervisorOptions} options\n */\n static handleMaxWorkingSandboxes (options)\n {\n function translateMWStoCores(maxWorkingSandboxes)\n {\n maxWorkingSandboxes = Number(maxWorkingSandboxes);\n if (typeof maxWorkingSandboxes !== 'undefined')\n options.cores.cpu = maxWorkingSandboxes;\n debugging('maxWorkingSandboxes') && console.debug(`translateMWStoCores: options.cores.cpu = ${options.cores.cpu}`);\n }\n\n translateMWStoCores(options.maxWorkingSandboxes);\n\n /* Slide in a maxWorkingSandboxes property whose backing store is v2-style configuration info. This property\n * is used by Worker applications that don't understand v2-style configuration yet.\n */\n Object.defineProperty(options, 'maxWorkingSandboxes', {\n get: () => options.cores.cpu,\n set: (value) => translateMWStoCores(value),\n configurable: true,\n enumerable: true,\n });\n }\n\n /** @type {OriginAccessManager} */\n get originManager () { return this.#supervisor.originManager; }\n //\n // This is the official supported interface for accessing supervisor.\n // When Sup1 dies, it will be simplified.\n //\n /** @type {Sandbox[]} */\n get sandboxes () { return this.#supervisor.sandboxes; }\n /** @type {Sandbox[]} */\n get workingSandboxes () { return this.#supervisor.workingSandboxes; }\n /** @type {Slice[]} */\n get slices () { return this.#supervisor.slices; }\n /** @type {Slice[]} */\n get queuedSlices () { return this.#supervisor.queuedSlices; }\n /** @type {Slice[]} */\n get workingSlices () { return this.#supervisor.workingSlices; }\n /** @type {Keystore} */\n get identityKeystore () { return this.#supervisor.identityKeystore; }\n /** @type {Keystore} */\n set identityKeystore (ks)\n {\n if (!(ks instanceof Keystore))\n throw new Error('Worker.identityKeystore: must be an instance of Keystore.');\n this.#supervisor.identityKeystore = ks;\n this.emit('identityChange', ks);\n }\n\n /** \n * @deprecated - use options.paymentAddress\n * @type {Address} \n */\n get paymentAddress () { return this.workerOptions.paymentAddress; }\n\n /** \n * @deprecated - use options.paymentAddress\n * @type {Address} \n */\n set paymentAddress (addr)\n {\n if (!(addr instanceof Address))\n throw new Error('Worker.paymentAddress: must be an instance of Address.');\n this.workerOptions.paymentAddress = addr;\n this.emit('paymentAddressChange', addr);\n }\n\n /** @type {opaqueId} */\n get workerId () { return this.#supervisor.workerId; }\n /** @type {opaqueId} */\n set workerId (id) { this.#supervisor.workerId = id; }\n /** @type {boolean} */\n get isSupervisor1 () { return this.#supervisor.isSupervisor1; } // This will die when we kill sup1\n /** @type {boolean} */\n get isSupervisor2 () { return this.#supervisor.isSupervisor2; } // This will die when we kill sup1\n \n /**\n * @deprecated\n * Please do not use this.\n * @type {Supervisor}\n */\n get badSupervisorBackdoor () { return this.#supervisor; }\n /** @type {SupervisorOptions|Options} */\n get options () { return this.#supervisor.options; }\n /**\n * Set the debug flag on the event emitter superclass of both WOrker and this.supervisor.\n * @type {boolean}\n */\n set enableDebugEvents (value) { this.debug = this.#supervisor.debug = value; }\n /**\n * Tries to fetch a task from TD (task distributer).\n * @todo Get rid of this (XXXpfr)\n * @param {number} [numCores]\n * @returns {Promise<any>}\n */\n fetchTask (numCores)\n {\n return this.#supervisor.fetchTask();\n }\n /**\n * Remove sandbox from supervisor and terminate it.\n * @param {Sandbox} sandbox\n */\n returnSandbox (sandbox)\n {\n return this.#supervisor.returnSandbox(sandbox);\n }\n /**\n * Remove sandbox from supervisor and return it to the scheduler.\n * @param {Slice} slice\n * @param {string} reason\n * @returns {Promise<any>}\n */\n returnSlice (slice, reason = 'unknown')\n {\n return this.#supervisor.returnSlice(slice, reason);\n }\n /**\n * Listen to eventName with eventHandler.\n * @param {*} eventName\n * @param {*} eventHandler\n */\n addSupervisorEventListener (eventName, eventHandler)\n {\n this.#supervisor.addListener(eventName, eventHandler);\n }\n /**\n * Stop listenting to eventName with eventHandler.\n * @param {*} eventName\n * @param {*} eventHandler\n */\n removeSupervisorEventListener (eventName, eventHandler)\n {\n this.#supervisor.removeListener(eventName, eventHandler);\n }\n /**\n * Get the job descriptor for jobAddress;\n * viz., the object value corresponding to the key jobAddress,\n * in the object returned by getJobsForTask in task-jobs.js.\n * @param {string} jobAddress\n * @returns {object}\n */\n jobDescriptor (jobAddress)\n {\n return this.#supervisor.jobDescriptor(jobAddress);\n }\n\n /**\n * Build an object suitable for use as options.cores based on the presumed number of logical cores in\n * the machine, and options.defaultCoreDensity. This is the only place options.defaultCoreDensity\n * should be used, all other calculations throughout the worker should be based on options.cores. The\n * default defaultCoreDensity is usually supplied via dcpConfig.worker in dcp-worker repo's\n * etc/dcp-worker-config.js \n *\n * options.hardware can be used to override the detected number of cores\n *\n * @note We current assume the machine has one GPU of infinite capability, and use evaluator capability\n * detection to use or not use this amazing GPU. That will obviously change as our capabilities \n * evolves.\n *\n * @note There are very important notes re core-count selection in How-supervisor-works.md.\n *\n * @note This is a static property of Worker because it is based on the physical properties of the\n * machine and the options object and does not have any relationship to a particular instance\n * of Worker.\n *\n * @param {SupervisorOptions} options\n * @returns {{ cpu: number, gpu: number }}\n */\n static defaultCores (options)\n {\n Worker.defaultInitialization(options);\n\n /** Create an \"active\" return object suitable for use as options.cores. Any writes to this object\n * persist as-is, but in the usual case, reading from the object causes the values to change\n * when hardware.vCores and/or defaultCoreDensity change.\n * @type {{ cpu: number, gpu: number }}\n */\n var ret = { cpu: 0, gpu: 0 };\n\n Object.defineProperty(ret, 'cpu', {\n get: () => Math.floor(options.defaultCoreDensity.cpu * options.hardware.vCores),\n set: (value) => {\n delete ret.cpu;\n ret.cpu = value;\n },\n configurable: true,\n enumerable: true,\n });\n\n Object.defineProperty(ret, 'gpu', {\n get: () => options.defaultCoreDensity.gpu * 1, /* one GPU until we know better */\n set: (value) => {\n delete ret.gpu;\n ret.gpu = value;\n },\n configurable: true,\n enumerable: true,\n });\n\n if (options.cores?.cpu)\n ret.cpu = options.cores.cpu;\n if (options.cores?.gpu)\n ret.gpu = options.cores.gpu;\n\n return ret;\n }\n \n /**\n * Required initialization.\n * @param {SupervisorOptions} options\n */\n static defaultInitialization (options)\n {\n if (!options.defaultCoreDensity)\n options.defaultCoreDensity = { cpu: 1, gpu: 0.75 };\n if (!options.hardware)\n options.hardware = { vCores: 0 };\n if (!options.hardware.vCores)\n {\n switch(dcpEnv.platform)\n {\n case 'nodejs':\n options.hardware.vCores = requireNative('os').cpus().length;\n break;\n case 'vanilla-web':\n case 'bravojs':\n options.hardware.vCores = navigator.hardwareConcurrency; /* can be less than actual hardware capabilities */\n\n if (options.hardware.vCores > 2 && /(Android).*(Chrome|Chromium)/.exec(navigator.userAgent))\n options.hardware.vCores = 2; /* Restricted on Android-Chrome due to bad OOM-handling; see DCP-3241 */\n\n if (navigator?.deviceMemory)\n options.hardware.vCores = Math.min(options.hardware.vCores, navigator.deviceMemory * 4); /* Restrict to max one vCore per 256MB RAM */\n break;\n default:\n options.hardware.vCores = 1;\n }\n }\n }\n\n /**\n * Legacy from Sup1.\n * @todo XXXpfr Kill when Sup1 dies.\n * @returns {void|Promise<void>}\n */\n setDefaultIdentityKeystore ()\n {\n if (this.isSupervisor1)\n return this.#supervisor.setDefaultIdentityKeystore();\n }\n}\n\nexports.Worker = Worker;\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?");
|
|
4607
|
+
eval("/**\n * @file worker/index.js This module implements the Worker API, used to create workers for \n * earning DCCs. \n *\n * To use Supervisor2 set the environment variable `USE_SUPERVISOR2` or \n * set dcpConfig.rollout.supervisor=2.\n *\n * @author Ryan Rossiter <ryan@kingsds.network>\n * Paul <paul@distributive.network>\n * @date May 2020\n * June, July 2022, Feb-April 2023\n * \n * @module dcp/worker\n * @access public\n * @copyright Copyright (c) 2018-2023, Distributive Corp. All Rights Reserved\n */\n// @ts-check\n\n\nconst debugging = (__webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope)('worker');\nconst { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\nconst { SchedMsg } = __webpack_require__(/*! dcp/dcp-client/schedmsg */ \"./src/dcp-client/schedmsg/index.js\");\nconst EventEmitter = __webpack_require__(/*! events */ \"./node_modules/events/events.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 { Address } = __webpack_require__(/*! dcp/dcp-client/wallet/eth */ \"./src/dcp-client/wallet/eth.js\");\nconst { Keystore } = __webpack_require__(/*! dcp/dcp-client/wallet/keystore */ \"./src/dcp-client/wallet/keystore.js\");\nconst dcpEnv = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\nconst dcp_timers = __webpack_require__(/*! dcp/common/dcp-timers */ \"./src/common/dcp-timers.js\");\nconst DISABLE_WORKER_CACHE_KEY = 'disable_worker';\nvar Supervisor; /* Supervisor ctor - this will die when we kill sup1 */\n\n// Supervisor2 typedefs\n/** @typedef {import('./supervisor2/slice2').Slice} Slice */\n/** @typedef {import('./supervisor2/sandbox2').Sandbox} Sandbox */\n/** @typedef {import('./supervisor2/options').Options} Options */\n\n// Supervisor1 typedefs\n// /** @typedef {import('./sandbox').Sandbox} Sandbox */\n// /** @typedef {import('./slice').Slice} Slice */\n\n/** @typedef {string} opaqueId */ // 22 character base64 string\n/** @typedef {import('dcp/dcp-client/wallet/eth').Address} Address */\n/** @typedef {import('dcp/dcp-client/wallet/keystore').Keystore} Keystore */\n/** @typedef {import('dcp/utils/jsdoc-types').SandboxOptions} SandboxOptions */\n/** @typedef {import('dcp/utils/jsdoc-types').SupervisorOptions} SupervisorOptions */\n/** @typedef {import('dcp/dcp-client/worker/origin-access-manager').OriginAccessManager} OriginAccessManager */\n\nfunction disableWorker ()\n{\n localStorage.setItem(DISABLE_WORKER_CACHE_KEY, true);\n}\n\n/** XXXwg remove this when we kill sup 1 */\nfunction determineSupervisorType()\n{\n if (Supervisor)\n return;\n\n if ( false\n || (__webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\").getenv)('USE_SUPERVISOR1')\n || (Number(dcpConfig.rollout?.supervisorVersion) === 1))\n Supervisor = (__webpack_require__(/*! ./supervisor */ \"./src/dcp-client/worker/supervisor.js\").Supervisor);\n else\n Supervisor = (__webpack_require__(/*! ./supervisor2 */ \"./src/dcp-client/worker/supervisor2/index.js\").Supervisor);\n}\n\n/**\n * Fired when the worker begins fetching slices from the scheduler.\n * @access public\n */\nclass Worker extends EventEmitter\n{\n /** Private class fields */\n #supervisor;\n #hasRef; /* event loop reference; undefined=>auto, false=>never, timer=>holding */\n\n /**\n * Returns a new Worker instance.\n * This constructor may alter the workerOptions object, for version-based polyfilling and/or providing\n * key API properties, such as the cores object.\n *\n * @access public\n * @param {Keystore} identity - identity of the Worker; used for signing messages to\n * task distributor etc.\n * @param {SupervisorOptions} workerOptions - options specified for the worker; may be mutated\n */\n constructor(identity, workerOptions)\n {\n super({ captureRejections: false });\n\n if (typeof workerOptions !== 'object')\n throw new Error('workerOptions argument must be object; is ' + typeof workerOptions);\n if (typeof identity !== 'object' || !(identity instanceof Keystore))\n throw new Error(`identity argument must be Keystore; is ${typeof identity === 'object' ? identity.constructor.name : typeof identity}`);\n\n determineSupervisorType();\n\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 * It is important that dynamic changes in workerOptions are reflected in Supervisor operations. In the case where\n * cores are derived solely from defaultCoreDensity, defaultCores provides an object which stays up to date with\n * defaultCoreDensity changes until a value is explicitly set.\n *\n * @type {SupervisorOptions}\n * @access public\n */\n this.workerOptions = workerOptions;\n if (!this.workerOptions.hasOwnProperty('cores') || !this.workerOptions.cores.hasOwnProperty('cpu'))\n this.workerOptions.cores = Worker.defaultCores(this.workerOptions);\n else\n Worker.defaultInitialization(this.workerOptions);\n if (this.workerOptions.hasOwnProperty('maxWorkingSandboxes') && this.workerOptions.maxWorkingSandboxes !== undefined)\n Worker.handleMaxWorkingSandboxes(this.workerOptions); /** XXXwg deprecate this API once all apps use sup2 conf */\n\n /* Correct cores object is now plumbed in; patently-absurd configs are changed to have 0 cores so users can debug */\n if (!(this.workerOptions.cores.cpu >= 0))\n this.workerOptions.cores.cpu = 0;\n if (!(this.workerOptions.cores.gpu >= 0))\n this.workerOptions.cores.gpu = 0;\n\n /**\n * @todo XXXpfr Rip out the sup2/sup1 special-casing when we finally kill sup1.\n * @type {Supervisor}\n * @access public\n */\n this.#supervisor = new Supervisor(this, identity, this.workerOptions);\n\n debugging() && console.debug(`Worker options(isSup2:${this.#supervisor.isSupervisor2}):`, this.workerOptions.cores.cpu, this.workerOptions.cores.gpu, this.workerOptions);\n }\n\n get supervisorVersion() { return this.#supervisor.version };\n\n /**\n * Disables worker instances from being started. The user will need to manually intervene to re-enable workers.\n * @access public\n */\n static disableWorker ()\n {\n disableWorker();\n }\n\n /**\n * Starts the worker.\n * @access public\n */\n async start()\n {\n debugging() && console.debug('Starting worker');\n if (this.working)\n {\n debugging() && console.debug('Worker already working, ignoring start request');\n return;\n }\n\n if (localStorage.getItem(DISABLE_WORKER_CACHE_KEY))\n {\n await confirmPrompt(`Worker has been disabled by the DCP Security Team; check the @DCP_Protocol Twitter feed for more information before continuing.`)\n if (await confirmPrompt('Are you sure you would like to restart the worker?'))\n {\n localStorage.removeItem(DISABLE_WORKER_CACHE_KEY);\n console.log(\"Starting worker...\");\n }\n else\n return;\n }\n \n try\n {\n let dontStart = false;\n\n this.emit('beforeStart', function cancel() { dontStart = true });\n if (dontStart)\n return;\n\n this.#supervisor.work();\n this.working = true;\n this.schedMsg.start();\n if (this.#hasRef !== false)\n this.ref();\n this.emit('start');\n }\n catch(error)\n {\n debugging() && console.error(error);\n this.emit('error', error);\n this.stop(true);\n }\n }\n\n /**\n * Stops the worker.\n * The reason for this.#hasRef === false state is for\n * await start\n * stop <-- #end isn't called until after the 2nd start\n * start\n * <-- #end is called here because #supervisor.stopWork is placed on the microtask queue.\n * @access public\n * @param {boolean} [forceTerminate=false] Whether the worker should stop imediately or allow the current slices to finish.\n */\n async stop(forceTerminate = false)\n {\n if (!this.working) \n {\n debugging() && console.debug('Worker already stopping, ignoring stop request');\n return;\n }\n this.working = false;\n\n try \n {\n // We're using synchronous event emitters, so if the abort function is called immediately,\n // then #supervisor.stopWork(forceTerminate) will be called with forceTerminate === true.\n this.emit('stop', function abort() { forceTerminate = true });\n\n debugging('shutdown') && console.debug(`Worker.stop(${forceTerminate}): stopping schedMsg...`);\n await this.schedMsg.stop();\n debugging('shutdown') && console.debug(`Worker.stop(${forceTerminate}): schedmsg stopped ok; stopping supervisor...`);\n\n return this.#supervisor.stopWork(forceTerminate).finally(() => {\n debugging('shutdown') && console.debug(`Worker.stop(${forceTerminate}): supervisor stopped ok`);\n this.#end();\n });\n }\n catch (error) \n {\n debugging() && console.error(error);\n this.emit('error', error);\n }\n }\n\n #end()\n {\n if (this.#hasRef !== false)\n {\n this.unref();\n this.ref();\n }\n this.emit('end');\n }\n \n /**\n * Set the worker's event loop reference to \"auto\" - the default behaviour - which holds a reference as\n * long as we are working.\n */\n ref()\n {\n if (this.#hasRef === false)\n this.#hasRef = undefined;\n if (this.#hasRef === undefined && this.working)\n {\n this.#hasRef = dcp_timers.setInterval(function workerRef(){}, 314159);\n debugging('refs') && console.debug('worker - making event-loop reference');\n }\n }\n\n /**\n * Tell the worker to not hold an event loop reference, whether it is working or not.\n */\n unref()\n {\n if (this.#hasRef)\n {\n dcp_timers.clearInterval(this.#hasRef);\n debugging('refs') && console.debug('worker - clearing event-loop reference');\n }\n this.#hasRef = false;\n }\n\n /**\n * We have been handed a v1-style configuration object, so we use it to generate v2-style info. Then\n * we add a getter/setter on maxWorkingSandboxes so that it always reflects cores.cpu, and so that its\n * mutations (from the worker-app) update cores.cpu.\n *\n * @deprecated\n * Get rid of this when all the workers are updated so that worker options do not have property maxWorkingSandboxes.\n * Please use options.defaultCoreDensity or options.cores instead.\n * @param {SupervisorOptions} options\n */\n static handleMaxWorkingSandboxes (options)\n {\n function translateMWStoCores(maxWorkingSandboxes)\n {\n maxWorkingSandboxes = Number(maxWorkingSandboxes);\n if (typeof maxWorkingSandboxes !== 'undefined')\n options.cores.cpu = maxWorkingSandboxes;\n debugging('maxWorkingSandboxes') && console.debug(`translateMWStoCores: options.cores.cpu = ${options.cores.cpu}`);\n }\n\n translateMWStoCores(options.maxWorkingSandboxes);\n\n /* Slide in a maxWorkingSandboxes property whose backing store is v2-style configuration info. This property\n * is used by Worker applications that don't understand v2-style configuration yet.\n */\n Object.defineProperty(options, 'maxWorkingSandboxes', {\n get: () => options.cores.cpu,\n set: (value) => translateMWStoCores(value),\n configurable: true,\n enumerable: true,\n });\n }\n\n /** @type {OriginAccessManager} */\n get originManager () { return this.#supervisor.originManager; }\n //\n // This is the official supported interface for accessing supervisor.\n // When Sup1 dies, it will be simplified.\n //\n /** @type {Sandbox[]} */\n get sandboxes () { return this.#supervisor.sandboxes; }\n /** @type {Sandbox[]} */\n get workingSandboxes () { return this.#supervisor.workingSandboxes; }\n /** @type {Slice[]} */\n get slices () { return this.#supervisor.slices; }\n /** @type {Slice[]} */\n get queuedSlices () { return this.#supervisor.queuedSlices; }\n /** @type {Slice[]} */\n get workingSlices () { return this.#supervisor.workingSlices; }\n /** @type {Keystore} */\n get identityKeystore () { return this.#supervisor.identityKeystore; }\n /** @type {Keystore} */\n set identityKeystore (ks)\n {\n if (!(ks instanceof Keystore))\n throw new Error('Worker.identityKeystore: must be an instance of Keystore.');\n this.#supervisor.identityKeystore = ks;\n this.emit('identityChange', ks);\n }\n\n /** \n * @deprecated - use options.paymentAddress\n * @type {Address} \n */\n get paymentAddress () { return this.workerOptions.paymentAddress; }\n\n /** \n * @deprecated - use options.paymentAddress\n * @type {Address} \n */\n set paymentAddress (addr)\n {\n if (!(addr instanceof Address))\n throw new Error('Worker.paymentAddress: must be an instance of Address.');\n this.workerOptions.paymentAddress = addr;\n this.emit('paymentAddressChange', addr);\n }\n\n /** @type {opaqueId} */\n get workerId () { return this.#supervisor.workerId; }\n /** @type {opaqueId} */\n set workerId (id) { this.#supervisor.workerId = id; }\n /** @type {boolean} */\n get isSupervisor1 () { return this.#supervisor.isSupervisor1; } // This will die when we kill sup1\n /** @type {boolean} */\n get isSupervisor2 () { return this.#supervisor.isSupervisor2; } // This will die when we kill sup1\n \n /**\n * @deprecated\n * Please do not use this.\n * @type {Supervisor}\n */\n get badSupervisorBackdoor () { return this.#supervisor; }\n /** @type {SupervisorOptions|Options} */\n get options () { return this.#supervisor.options; }\n /**\n * Set the debug flag on the event emitter superclass of both WOrker and this.supervisor.\n * @type {boolean}\n */\n set enableDebugEvents (value) { this.debug = this.#supervisor.debug = value; }\n /**\n * Tries to fetch a task from TD (task distributer).\n * @todo Get rid of this (XXXpfr)\n * @param {number} [numCores]\n * @returns {Promise<any>}\n */\n fetchTask (numCores)\n {\n return this.#supervisor.fetchTask();\n }\n /**\n * Remove sandbox from supervisor and terminate it.\n * @param {Sandbox} sandbox\n */\n returnSandbox (sandbox)\n {\n return this.#supervisor.returnSandbox(sandbox);\n }\n /**\n * Remove sandbox from supervisor and return it to the scheduler.\n * @param {Slice} slice\n * @param {string} reason\n * @returns {Promise<any>}\n */\n returnSlice (slice, reason = 'unknown')\n {\n return this.#supervisor.returnSlice(slice, reason);\n }\n /**\n * Listen to eventName with eventHandler.\n * @param {*} eventName\n * @param {*} eventHandler\n */\n addSupervisorEventListener (eventName, eventHandler)\n {\n this.#supervisor.addListener(eventName, eventHandler);\n }\n /**\n * Stop listenting to eventName with eventHandler.\n * @param {*} eventName\n * @param {*} eventHandler\n */\n removeSupervisorEventListener (eventName, eventHandler)\n {\n this.#supervisor.removeListener(eventName, eventHandler);\n }\n /**\n * Get the job descriptor for jobAddress;\n * viz., the object value corresponding to the key jobAddress,\n * in the object returned by getJobsForTask in task-jobs.js.\n * @param {string} jobAddress\n * @returns {object}\n */\n jobDescriptor (jobAddress)\n {\n return this.#supervisor.jobDescriptor(jobAddress);\n }\n\n /**\n * Build an object suitable for use as options.cores based on the presumed number of logical cores in\n * the machine, and options.defaultCoreDensity. This is the only place options.defaultCoreDensity\n * should be used, all other calculations throughout the worker should be based on options.cores. The\n * default defaultCoreDensity is usually supplied via dcpConfig.worker in dcp-worker repo's\n * etc/dcp-worker-config.js \n *\n * options.hardware can be used to override the detected number of cores\n *\n * @note We current assume the machine has one GPU of infinite capability, and use evaluator capability\n * detection to use or not use this amazing GPU. That will obviously change as our capabilities \n * evolves.\n *\n * @note There are very important notes re core-count selection in How-supervisor-works.md.\n *\n * @note This is a static property of Worker because it is based on the physical properties of the\n * machine and the options object and does not have any relationship to a particular instance\n * of Worker.\n *\n * @param {SupervisorOptions} options\n * @returns {{ cpu: number, gpu: number }}\n */\n static defaultCores (options)\n {\n Worker.defaultInitialization(options);\n\n /** Create an \"active\" return object suitable for use as options.cores. Any writes to this object\n * persist as-is, but in the usual case, reading from the object causes the values to change\n * when hardware.vCores and/or defaultCoreDensity change.\n * @type {{ cpu: number, gpu: number }}\n */\n var ret = { cpu: 0, gpu: 0 };\n\n Object.defineProperty(ret, 'cpu', {\n get: () => Math.floor(options.defaultCoreDensity.cpu * options.hardware.vCores),\n set: (value) => {\n delete ret.cpu;\n ret.cpu = value;\n },\n configurable: true,\n enumerable: true,\n });\n\n Object.defineProperty(ret, 'gpu', {\n get: () => options.defaultCoreDensity.gpu * 1, /* one GPU until we know better */\n set: (value) => {\n delete ret.gpu;\n ret.gpu = value;\n },\n configurable: true,\n enumerable: true,\n });\n\n if (options.cores?.cpu)\n ret.cpu = options.cores.cpu;\n if (options.cores?.gpu)\n ret.gpu = options.cores.gpu;\n\n return ret;\n }\n \n /**\n * Required initialization.\n * @param {SupervisorOptions} options\n */\n static defaultInitialization (options)\n {\n if (!options.defaultCoreDensity)\n options.defaultCoreDensity = { cpu: 1, gpu: 0.75 };\n if (!options.hardware)\n options.hardware = { vCores: 0 };\n if (!options.hardware.vCores)\n {\n switch(dcpEnv.platform)\n {\n case 'nodejs':\n options.hardware.vCores = requireNative('os').cpus().length;\n break;\n case 'vanilla-web':\n case 'bravojs':\n options.hardware.vCores = navigator.hardwareConcurrency; /* can be less than actual hardware capabilities */\n\n if (options.hardware.vCores > 2 && /(Android).*(Chrome|Chromium)/.exec(navigator.userAgent))\n options.hardware.vCores = 2; /* Restricted on Android-Chrome due to bad OOM-handling; see DCP-3241 */\n\n if (navigator?.deviceMemory)\n options.hardware.vCores = Math.min(options.hardware.vCores, navigator.deviceMemory * 4); /* Restrict to max one vCore per 256MB RAM */\n break;\n default:\n options.hardware.vCores = 1;\n }\n }\n }\n\n /**\n * Legacy from Sup1.\n * @todo XXXpfr Kill when Sup1 dies.\n * @returns {void|Promise<void>}\n */\n setDefaultIdentityKeystore ()\n {\n if (this.isSupervisor1)\n return this.#supervisor.setDefaultIdentityKeystore();\n }\n}\n\nexports.Worker = Worker;\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?");
|
|
4608
4608
|
|
|
4609
4609
|
/***/ }),
|
|
4610
4610
|
|
|
@@ -4724,7 +4724,7 @@ eval("/**\n * @file dcp/src/dcp-client/worker/supervisor2/gpu_support.js\n *\n *
|
|
|
4724
4724
|
/***/ ((__unused_webpack_module, exports, __webpack_require__) => {
|
|
4725
4725
|
|
|
4726
4726
|
"use strict";
|
|
4727
|
-
eval("/**\n * @file dcp/src/dcp-client/worker/supervisor2/index.js\n * Code managing sandboxes, tasks, jobs, and slices within in a DCP Worker.\n * @author Wes Garland, wes@distributive.network\n * Paul, paul@distributive.network\n * @date Dec 2020\n * June 2022, Jan-April 2023\n * @module supervisor\n * @copyright Copyright (c) 2018-2023, Distributive Corp. All Rights Reserved\n */\n/*\n * initial ready reconnecting stopping stopped paused broken\n * |-- ctor ----------------------------------------------------------------------------------------------------------------->\n * |-- work ----------------------------------------------------------------------------------------------------------------->\n * |-- work --------------------------------------------------------------------------------------------------->\n * |-- work -------------------------------------------------------------------------------->\n * |-- work --------------------------------------------------------->\n * |-- work --------------------------------->\n * |-- Worker.pause --------------------------------------------------------------------------->\n * <-- Worker.unpause -------------------------------------------------------------------------|\n * |-- work ----->\n * |-- stopWork ---------------------------->\n * |-- postStopShutdown --->\n * |-- PM.connectTo --> (ProtocolManager)\n * <-- PM.connectTo --| (ProtocolManager)\n * |-- stopWork ------------------------------------------->\n * <-- work -----------------------------------------------------------------------|\n * <-- stopWork -----------------------------------------------------|\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 DCP_ENV = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\nconst constants = __webpack_require__(/*! dcp/common/scheduler-constants */ \"./src/common/scheduler-constants.js\");\nconst { assert } = __webpack_require__(/*! dcp/common/dcp-assert */ \"./src/common/dcp-assert.js\");\nconst dcp_timers = __webpack_require__(/*! dcp/common/dcp-timers */ \"./src/common/dcp-timers.js\");\nconst EventEmitter = __webpack_require__(/*! events */ \"./node_modules/events/events.js\");\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\nconst { Address } = __webpack_require__(/*! dcp/dcp-client/wallet/eth */ \"./src/dcp-client/wallet/eth.js\");\nconst { Keystore } = __webpack_require__(/*! dcp/dcp-client/wallet/keystore */ \"./src/dcp-client/wallet/keystore.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 { 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 { OriginAccessManager } = __webpack_require__(/*! dcp/dcp-client/worker/origin-access-manager */ \"./src/dcp-client/worker/origin-access-manager.js\");\nconst { a$sleepMs, booley, toJobMap, encodeDataURI, stringify, nextEma } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\nconst kvin = __webpack_require__(/*! kvin */ \"./node_modules/kvin/kvin.js\");\n\nconst { ModuleCache } = __webpack_require__(/*! ./module-cache */ \"./src/dcp-client/worker/supervisor2/module-cache.js\");\nconst { Promise_any } = __webpack_require__(/*! ./promise_any */ \"./src/dcp-client/worker/supervisor2/promise_any.js\");\nconst { ProtocolManager } = __webpack_require__(/*! ./protocol-manager */ \"./src/dcp-client/worker/supervisor2/protocol-manager.js\");\nconst { EvaluatorManager } = __webpack_require__(/*! ./evaluator-manager */ \"./src/dcp-client/worker/supervisor2/evaluator-manager.js\");\nconst { DelayManager } = __webpack_require__(/*! ./delay-manager */ \"./src/dcp-client/worker/supervisor2/delay-manager.js\");\nconst { Options } = __webpack_require__(/*! ./options */ \"./src/dcp-client/worker/supervisor2/options.js\");\nconst common = __webpack_require__(/*! ./common */ \"./src/dcp-client/worker/supervisor2/common.js\");\nconst { debugBuild, selectiveDebug, selectiveDebug2, minimalDiag, selectiveSupEx } = common;\nconst supShared = __webpack_require__(/*! ../SupShared */ \"./src/dcp-client/worker/SupShared.js\");\nconst { canScheduleGPU } = __webpack_require__(/*! ./gpu_support */ \"./src/dcp-client/worker/supervisor2/gpu_support.js\");\n\n/** @typedef {string} opaqueId */ // 22 character base64 string\n/** @typedef {import('./slice2').Slice} Slice */\n/** @typedef {import('dcp/utils/jsdoc-types').Auth} Auth */\n/** @typedef {import('dcp/utils/jsdoc-types').Body} Body */\n/** @typedef {import('./sandbox2').SandboxHandle} SandboxHandle */\n/** @typedef {import('dcp/utils/jsdoc-types').SliceObj} SliceObj */\n/** @typedef {import('dcp/dcp-client/worker/index').Worker} Worker */\n/** @typedef {import('dcp/utils/jsdoc-types').TDPayload} TDPayload */\n/** @typedef {import('dcp/utils/jsdoc-types').Signature} Signature */\n/** @typedef {import('dcp/utils/jsdoc-types').SliceMessage} SliceMessage */\n/** @typedef {import('dcp/dcp-client/wallet/keystore').Keystore} Keystore */\n/** @typedef {import('dcp/utils/jsdoc-types').SupervisorOptions} SupervisorOptions */\n/** @typedef {import('dcp/protocol-v4/connection/connection').Connection} Connection */\n\n//\n// Index to functionality -- search for '_Idx' to toggle through the index.\n//\n// 1) class Supervisor\n// 2) work, checkCapabilities\n// 3) safeEmit, workerEmit, jobEmit, error, warning, mungeError, jobDescriptor, setState\n// 4) returnAllSlices, postStopShutdown, abort, stopWork, purgeJob\n// 5) roundRobinSlices, makeJobSelectionCursor, handleSandboxWorkError, handleFailedSlice\n// 6) returnSlices, returnSlice, emitProgressReport\n// 7) jobQuanta, repoMan, predictLoad(viz., clairvoyance), waitUntilWorkIsReady, generateWorkerComputeGroups\n// 8) availableSandboxSpace, fetchTask, addTaskToWorkload, fetchFromTD, clearUnusedJobManagersAndModuleCache\n// 9) createSandbox, returnSandbox, hookUpSandboxListeners, pruneSandboxes\n// 10) recordResult, sendToResultSubmitter, sendResultToRemote\n// 11) handleWorkReject\n//\n\n// _Idx\n//\n// class Supervisor\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 * Possible states: 'initial', 'ready', 'reconnecting', 'stopping', 'stopped', 'broken'\n * Start state:\n * - initial\n *\n * Intermediate states:\n * - ready\n * - reconnecting\n * - stopping\n *\n * Terminal states:\n * - stopped\n * - broken\n *\n * Valid transitions:\n * - initial -> ready -> reconnecting -> ready\n * - ready -> stopping -> stopped\n * - initial -> broken\n */\nclass Supervisor extends EventEmitter\n{\n /**\n * @constructor\n * @param {Worker} worker\n * @param {Keystore} identity\n * @param {SupervisorOptions} options\n */\n constructor (worker, identity, options)\n {\n super({ captureRejections: false });\n\n if (!(identity instanceof Keystore))\n throw new Error(`identity ${JSON.stringify(identity)} must be an instance of Keystore`);\n\n debugging('supervisor') && console.debug('Supervisor.options', options);\n assert(options === worker.workerOptions);\n \n /** @type {Worker} */\n this.worker = worker;\n /** @type {Keystore} */\n this.identityKeystore = identity;\n /** @type {Options} */\n this.options = new Options(options, worker);\n\n selectiveDebug() && console.debug('Supervisor: cores.cpu, cores.gpu, maxSandboxes', options.cores?.cpu, options.cores?.gpu, this.options.maxSandboxes);\n\n /** @type {ModuleCache} */\n this.moduleCache = new ModuleCache(this);\n\n // Manage delays and exponential backoff.\n this.delayManager = new DelayManager(this, this.options.defaultDelayIncrement);\n\n /* See https://distributive.atlassian.net/browse/DCP-3175 */\n /** @type {OriginAccessManager} */\n this.originManager = OriginAccessManager.construct(this.options.allowOrigins);\n\n /** @type {ProtocolManager} */\n this.dcp4 = new ProtocolManager(this);\n\n /** @type {common.DebuggingTools} */\n this.dbg = new common.DebuggingTools(this);\n\n // Turn on for max speed debugging.\n if (false)\n {}\n\n /** @type { Synchronizer } */\n this.state = new Synchronizer('initial', [ 'initial', 'ready', 'reconnecting', 'stopping', 'stopped', 'paused', 'broken']);\n /** @type {Object<string, JobManager>} */\n this.jobMap = {}; // jobAddress => jobManager\n\n /** @type {JobManager[]} */\n this.jobManagerInventory = common.InventoryArray('jobManagers');\n /** @type {Sandbox[]} */\n this.sandboxInventory = []; // All sandboxes that are being used by the job managers. Makes sure we don't lose sandboxes.\n /** @type {{ next: cbNext, push: cbPush }} */\n this.cursor = null;\n /** @type {number} */\n this.defaultQuanta = 1.0;\n\n /**\n * Evaluator down management.\n **/\n this.evaluator = new EvaluatorManager();\n\n // There are 2 kinds of barriers.\n // 1) fetchTaskBarrier is a barrier for the task fetching from task distributor path.\n // 2) roundRobinBarrier is a barrier for the slice execution path.\n /** @type {boolean} */\n this.fetchTaskBarrier = false;\n /** @type {boolean} */\n this.roundRobinBarrier = false;\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(200); // N = 200 should be more than enough.\n /**\n * When true we await waitUntilWorkIsReady until at least 1 job is ready with at least 1 ready slice.\n * waitUntilWorkIsReady\n * @type {boolean}\n */\n this.waitForWork = true;\n /**\n * Last repoMan time stamp.\n * @type {number}\n **/\n this.lastRepoMan = Date.now();\n /**\n * Last prune time stamp.\n * @type {number}\n **/\n this.lastPrune = Date.now();\n /**\n * General time stamp.\n * @type {number}\n **/\n this.lastTime = Date.now();\n /**\n * Fetch started time stamp.\n * @type {number}\n **/\n this.fetchTaskStarted = 0;\n /**\n * The capabilities of a random sandbox.\n * @todo XXXpfr Re-work this once fetchTask uses the capabilities of every sandbox to fetch slices.\n * @type {object}\n */\n this.capabilities = null;\n /**\n * EMA times series of CPUTime + GPUTime over all jobs.\n * Each EMA entry is computed right before calling fetchTask.\n * @type {number}\n */\n this.localTime = 0;\n /**\n * EMA times series of sliceCPUTime + sliceGPUTime over all jobs.\n * Each EMA entry is computed right before calling fetchTask.\n * @type {number}\n */\n this.globalTime = 0;\n /**\n * When this.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\n try\n {\n // Start up the connections.\n this.dcp4.instantiateAllConnections();\n }\n catch(error)\n {\n this.error('Failed to set up DCP connections:', error);\n this.setState('initial', 'broken');\n this.worker.stop(true).finally(() => { throw error; });\n }\n }\n\n //\n // Compatibility layer between Sup1, Sup2 and the Sup interface exposed by Worker.\n //\n /**\n * Get all sandboxes.\n * @type {Sandbox[]}\n */\n get sandboxes () { return this.sandboxInventory.filter((sandbox) => !sandbox.isTerminated); }\n /**\n * Get all working sandboxes.\n * @type {Sandbox[]}\n */\n get workingSandboxes () { return this.sandboxInventory.filter((sandbox) => sandbox.isWorking); }\n /**\n * Get the number of working sandboxes.\n * @type {number}\n */\n get workingSandboxCount () { return this.workingSandboxes.length; }\n /**\n * Get all slices over all jobs..\n * @type {Slice[]}\n */\n get slices () {\n const slices = [];\n this.jobManagerInventory.forEach((jobManager) => { slices.push(...jobManager.sliceInventory); });\n return slices;\n }\n /**\n * Get all queued slices over all jobs..\n * @type {Slice[]}\n */\n get queuedSlices () {\n const slices = [];\n this.jobManagerInventory.forEach((jobManager) => { slices.push(...jobManager.queuedSlices); });\n return slices;\n }\n /**\n * Get all queued slices over all jobs..\n * @type {Slice[]}\n */\n get workingSlices () {\n const slices = [];\n this.jobManagerInventory.forEach((jobManager) => { slices.push(...jobManager.workingSlices); });\n return slices;\n }\n /** @type {opaqueId} */\n get workerId () { return this.options.workerId; }\n /** @type {opaqueId} */\n set workerId (id) { this.options.workerId = id; }\n get version() { return '2.0.0' }\n /**\n * @deprecated\n * @todo XXXpfr Rip out this sup2/sup1 special-casing when we finally kill sup1.\n * @type {boolean}\n */\n get isSupervisor1 () { return false; }\n /**\n * @deprecated\n * @todo XXXpfr Rip out this sup2/sup1 special-casing when we finally kill sup1.\n * @type {boolean}\n */\n get isSupervisor2 () { return true; }\n\n //\n // Miscellaneous properties.\n //\n\n /**\n * Dynamic maxWorkingCores.\n * The maximum number of cores that can be executing slices. Slices are scheduled\n * using density. E.g. suppose a job has GPUDensity is 0 and CPUDensity is 0.5,\n * then 2 slices of this job can be scheduled on a single core.\n * @type {number}\n */\n get maxWorkingCores () { return this.options.cores?.cpu; }\n /**\n * Dynamic maxWorkingGPUs.\n * The maximum number of GPUs that can be executing slices. Slices are scheduled\n * using density. E.g. suppose a job has GPUDensity is 0.5 and CPUDensity is 0.5,\n * then 2 slices of this job can be scheduled on a single GPU core and a single CPU core.\n * @type {number}\n */\n get maxWorkingGPUs () { return this.options.cores?.gpu; }\n /**\n * @deprecated\n * @todo XXXpfr Get rid of this after Sup1 dies.\n */\n get lastDcpsid () { return this.dcp4.lastDcpsid; }\n /**\n * @deprecated\n * @todo XXXpfr Get rid of this after Sup1 dies.\n */\n set lastDcpsid (dcpsid) { this.dcp4.lastDcpsid = dcpsid; }\n /**\n * Indicates whether supervisor is ready for business.\n * @type {boolean}\n */\n get isReady () { return this.worker.working && this.state.is('ready'); }\n /**\n * The # of sandboxes not being used.\n * @type {number}\n */\n get unusedSandboxCount () { return this.options.maxSandboxes - this.workingSliceCount; }\n /**\n * The unused amount of CPU density in the cores.\n * @type {number}\n */\n get unusedCoreSpace () { return this.maxWorkingCores - this.workingSliceDensity; }\n /**\n * The unused amount of GPU density in the cores.\n * Use Math.max(1, this.maxWorkingGPUs) so there's always enough room to schedule\n * a GPU slice when this.workingGPUDensity = 0. In RoundRobinSlices we use the accumulated\n * recent history ( canScheduleGPU(maxWorkingGPUs) ) to check whether the average recent\n * density is within this.maxWorkingGPUs.\n * @type {number}\n */\n get unusedGPUSpace () { return Math.max(1, this.maxWorkingGPUs) - this.workingGPUDensity; }\n /** @type {number} */\n get workingSliceDensity ()\n {\n let density = 0;\n for (const jobMan of this.jobManagerInventory)\n density += jobMan.workingSliceDensity;\n return density;\n }\n /** @type {number} */\n get workingGPUDensity ()\n {\n let density = 0;\n for (const jobMan of this.jobManagerInventory)\n density += jobMan.workingGPUDensity;\n return density;\n }\n /** @type {number} */\n get workingSliceCount ()\n {\n let count = 0;\n for (const jobMan of this.jobManagerInventory)\n count += jobMan.workingSliceCount;\n return count;\n }\n /**\n * Compute the estimated time to completion of all work.\n * The time is measured as if there were only a single slice running at a time.\n * workRemaining is the amount of time until completion.\n * @type {number}\n */\n get workRemaining ()\n {\n let workRemaining = 0;\n for (const jobMan of this.jobManagerInventory)\n workRemaining += jobMan.workRemaining;\n return workRemaining;\n }\n\n // _Idx\n //\n // work, checkCapabilities\n //\n\n /**\n * Set up sandboxes and interval timers, then start to search for work.\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 */\n work ()\n {\n const abort = async (error) => {\n // May be in a stopping/stopped state, because dcp-worker was hit with ctrl-C.\n this.setState(['ready', 'stopping', 'stopped', 'reconnecting'], 'broken');\n await this.worker.stop(true);\n throw error;\n };\n /* Provide opportunity for calling code to hook ready/error events. */\n dcp_timers.setImmediate(() => {\n try\n {\n // [ 'initial', 'ready', 'reconnecting', 'stopping', 'stopped', 'paused', 'broken' ]\n if (this.state.isNot('initial'))\n {\n if (this.state.in(['ready', 'stopping', 'reconnecting']))\n {\n this.warning(`Supervisor.work was called when supervisor is already ${this.state.valueOf()}.`, 'Please either wait and try again or restart worker.');\n return;\n }\n else if (this.state.is('broken'))\n {\n this.warning(\"Cannot call Supervisor.work when supervisor is in a 'broken' state. Please restart worker.\");\n return;\n }\n this.state.set(['stopped', 'paused'], 'initial');\n }\n this.evaluator.initialize();\n this.dcp4.instantiateAllConnections();\n\n // Beacon interval timer.\n this.progressReportTimer = dcp_timers.setInterval(() => this.emitProgressReport(), this.options.progressReportInterval);\n // Watchdog: fetchTask-driven interval timer.\n this.watchdogTimer = dcp_timers.setInterval(() => this.fetchTask(), this.options.watchdogInterval);\n\n // Interval timers helps keep workers and localExec alive forever.\n this.progressReportTimer.unref();\n this.watchdogTimer.unref();\n\n if ( false || debugging('supervisor'))\n {\n this.sliceDebuggingTimer = setInterval(() => {\n this.jobManagerInventory.forEach((jobMan) => {\n const { unassigned, ready, reserved, working, workdone, complete, failed, finished } = jobMan.dumpSlices ('RRS', false, false);\n console.debug(`RRS(${jobMan.identifier},${this.unusedSandboxCount},${this.unusedCoreSpace},${this.workingSliceCount},${this.workingSliceDensity}): u/r/rsv/w/wd/c/f/fsh ${unassigned}/${ready}/${reserved}/${working}/${workdone}/${complete}/${failed}/${finished}`, jobMan.identifier, this.sandboxInventory.length);\n });\n }, 30 * 1000);\n if (this.sliceDebuggingTimer.unref)\n this.sliceDebuggingTimer.unref();\n }\n\n this.state.set('initial', 'ready');\n\n // Create 1 sandbox now to get the capabilities which are sent to Task Distributor by fetchTask.\n this.createSandbox()\n .then((sandbox) => {\n this.sandboxInventory.push(sandbox);\n debugging('supervisor') && console.debug('work() after createSandbox', this.sandboxInventory.length, sandbox.identifier, Date.now() - this.lastTime, this.options.watchdogInterval);\n this.fetchTask() // Don't wait for watchdog.\n .catch (async (error) => {\n this.error('work() failed when calling fetchTask', error);\n await abort(error);\n });\n })\n .catch(async (error) => {\n this.error('work() failed when calling createSandbox, exiting...', error);\n await abort(error);\n });\n }\n catch(error)\n {\n this.error('work() failed', error);\n if (this.state.is('initial')) this.state.set('initial', 'broken');\n else if (!this.state.is('broken')) this.setState('ready', 'broken');\n this.worker.stop(true).finally(() => { throw error; });\n }\n });\n }\n\n /** Construct capabilities when necessary. */\n checkCapabilities (sandbox)\n {\n /**\n * Assign the capabilities of one the sandboxes before fetching slices from the scheduler.\n * @todo XXXpfr Re-work this once fetchTask uses the capabilities of every sandbox to fetch slices.\n */\n this.capabilities = sandbox.capabilities;\n if (DCP_ENV.isBrowserPlatform && this.capabilities.browser)\n this.capabilities.browser.chrome = DCP_ENV.isBrowserChrome;\n\n debugging('supervisor') && console.debug('Supervisor.checkCapabilities computed', Date.now() - this.lastTime);\n }\n\n // _Idx\n //\n // safeEmit, workerEmit, jobEmit,\n // error, warning, mungeError, jobDescriptor, setState\n //\n\n /**\n * Safe event emitter.\n * @param {EventEmitter} emitter\n * @param {string} event\n * @param {...any} args\n */\n safeEmit(emitter, event, ...args)\n {\n try\n {\n emitter.emit(event, ...args);\n }\n catch (error)\n {\n this.error(`Event handler for event ${event} threw an exception`, error);\n }\n }\n\n /**\n * Safe event emitter on worker.\n * @param {string} event\n * @param {...any} args\n */\n workerEmit(event, ...args)\n {\n this.safeEmit(this.worker, event, ...args);\n }\n\n /**\n * Safe event emitter on slice.jobHandle.\n * @param {Slice} slice\n * @param {string} event\n * @param {...any} args\n */\n jobEmit(slice, event, ...args)\n {\n this.safeEmit(slice.jobHandle, event, ...args);\n }\n\n /**\n * Error feedback to user.\n * @param {string} message\n * @param {Array<Error>|Error|string} [coreError]\n * @param {string} [additionalInfo]\n * @param {boolean} [supressStack=false]\n */\n error (message, coreError, additionalInfo, supressStack = false)\n {\n const isString = (s) => { return (typeof s === 'string' || s instanceof String); };\n if (coreError instanceof AggregateError)\n coreError = coreError.errors;\n if (Array.isArray(coreError) && coreError.length > 0) // Emit error for every element of array.\n return coreError.flat().forEach((c_err) => this.error(message, c_err, additionalInfo));\n\n debugging('supervisor') && console.debug('Supervisor.error:', message, coreError, additionalInfo);\n if (!message)\n message = 'Supervisor.error called w/o valid message';\n if (additionalInfo)\n {\n if (typeof additionalInfo === 'object')\n // @ts-ignore\n additionalInfo = (additionalInfo instanceof Error) ? additionalInfo.message : JSON.stringify(additionalInfo);\n else if (typeof additionalInfo !== 'string')\n additionalInfo = String(additionalInfo);\n\n if (!isString(additionalInfo))\n additionalInfo = additionalInfo.toString();\n if (!coreError)\n coreError = '';\n else if (!isString(coreError))\n coreError = String(coreError);\n }\n\n let dcpError;\n if (additionalInfo)\n dcpError = new DCPError(message, coreError, additionalInfo, supressStack);\n else if (coreError && (coreError instanceof Error))\n dcpError = new DCPError(message, coreError, '', supressStack);\n else\n dcpError = new DCPError(message, '', '', supressStack);\n\n this.worker.emit('error', dcpError);\n }\n\n /**\n * Warning feedback to user.\n * @param {string[]} messages\n */\n warning (...messages)\n {\n debugging('supervisor') && console.debug('Supervisor.warning:', messages);\n if (messages.length < 1)\n messages = [ 'Supervisor.warning called w/o valid message(s)' ];\n messages.forEach((message) => this.worker.emit('warning', message));\n }\n\n /**\n * @deprecated\n * Create new object and copy the interesting properties from error.\n * Only show the stack for debug builds.\n * If timestamp isn't set, assign new Date().\n * @param {{ message }|string|object} error\n * @param {*} [errorCtor]\n * @returns {string|{ message }}\n */\n __mungeError (error, errorCtor)\n {\n if (typeof error === 'string')\n {\n const errorLines = error.split('\\n');\n return common.displayMaxDiagInfo() ? error : errorLines[0];\n }\n\n if (!error || typeof error !== 'object' || !('message' in error) || Array.isArray(error))\n return error;\n\n if (minimalDiag)\n return error.message;\n\n const errorObj = errorCtor ? new errorCtor(error.message) : { message: error.message };\n\n const props = common.displayMaxDiagInfo()\n ? [ 'type', 'process', 'name', 'origin', 'info', 'code', 'errorCode', 'operation', 'fileName', 'lineNumber', 'timestamp' ]\n : [ 'code', 'errorCode', 'fileName', 'lineNumber', 'timestamp' ]\n const predCopy = (prop) => {\n if (error[prop])\n errorObj[prop] = error[prop];\n };\n\n props.forEach((prop) => { predCopy(prop); });\n\n if (common.displayMaxDiagInfo())\n {\n predCopy('stack');\n if (errorObj['name'] === 'Error')\n delete errorObj['name'];\n }\n if (!errorObj['timestamp'])\n errorObj['timestamp'] = new Date();\n\n return errorObj;\n }\n\n /**\n * Get the job descriptor for the appropriate job manager,\n * which is the object value corresponding to jobAddress, in\n * the object returned by getJobsForTask in task-jobs.js.\n * @param {string} jobAddress\n * @returns {object}\n */\n jobDescriptor (jobAddress)\n {\n const jobManager = this.jobMap[jobAddress];\n if (!jobManager)\n throw new Error(`Cannot find the job descriptor corresponding to jobAddress ${jobAddress}`);\n return jobManager.jobMessage;\n }\n\n /**\n * Protect this.state when transitioning from currState -> nextState\n * It's dangerous to place this.state.set in a catch block with this.error or this.warning\n * because an uncaught exception will kill process before emitting the event-based diagnostic.\n * @param {string|string[]} currState\n * @param {string} nextState\n */\n setState(currState, nextState)\n {\n try { this.state.set(currState, nextState); }\n catch (e) { this.error('Supervisor.state.set error', e); }\n }\n\n // _Idx\n //\n // returnAllSlices, postStopShutdown, abort\n // stopWork, purgeJob\n //\n\n /** @returns {Promise<*>} */\n returnAllSlices ()\n {\n if (selectiveDebug())\n {\n const activeSlices = this.jobManagerInventory.map((jm) => jm.activeSlices).flat();\n if (activeSlices.length > 0)\n this.warning(`Returning active slices : ${stringify(activeSlices.map((slice) => slice.identifier), -1, 2)}`);\n }\n // The promises are all about returning the slices to the scheduler and there's no reason to await that.\n return Promise.all(this.jobManagerInventory.map((jm) => jm.destroy()));\n }\n\n /** @returns {Promise<*>} */\n postStopShutdown ()\n {\n for (const sandbox of this.sandboxInventory)\n sandbox.terminate(false);\n this.sandboxInventory = [];\n\n // There shouldn't be anything in the job managers, but just to be safe call returnAllSlices.\n // Clear jobManagerInventory, close all connections and set state to 'stopped'.\n return this.returnAllSlices()\n .finally(() => {\n // Re-enable is-screen-saver-active logic for the sandbox handle 'end' event handler.\n this.evaluator.pauseSandboxHandleEndHandler = false;\n this.jobManagerInventory = common.InventoryArray('jobManagers');\n return this.dcp4.closeConnections()\n .finally (() => {\n if (this.state.isNot('stopped'))\n this.setState('stopping', 'stopped');\n // This log message assume slices were returned to scheduler in a previous operation, which is the only current use case.\n // If we use this function in a different way in the future, update the log message.\n selectiveDebug() && console.debug(`Supervisor.postStopShutdown(${this.state}): terminated all sandboxes and returned all slices to scheduler...`);\n });\n });\n }\n\n /**\n * Stop the worker immediately and return all unfinished slices.\n * @returns {Promise<*>}\n */\n abort ()\n {\n return this.returnAllSlices()\n .finally (() => {\n return this.postStopShutdown();\n });\n }\n\n /**\n * Terminates sandboxes and returns slices.\n * Sets the working flag to false, call @this.work to start working again.\n *\n * If forceTerminate is true: Terminates all sandboxes and returns all slices.\n * If forceTerminate is false: Terminates non-working sandboxes and returns 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<*>}\n */\n async stopWork (forceTerminate = true)\n {\n /** @returns {boolean} */\n const doNotWaitForWork = () => {\n return (this.evaluator.reallyDown || !this.sandboxInventory.filter(sbx => !sbx.isTerminated).length);\n }\n selectiveDebug() && console.debug(`Supervisor.stopWork(${forceTerminate}, ${this.state}): terminating sandboxes and returning slices to scheduler.`);\n\n // [ 'initial', 'ready', 'reconnecting', 'stopping', 'stopped', 'paused', 'broken']\n if (this.state.in(['stopping', 'stopped', 'reconnecting']))\n {\n this.warning(`Supervisor.stopWork was called when supervisor is in state ${this.state.valueOf()}.`, 'Please either wait and try again or restart worker.');\n return;\n }\n else if (this.state.is('initial'))\n {\n this.warning('Cannot call stopWork before worker has started. Please either wait and try again or restart worker.');\n return;\n }\n this.state.set(['ready', 'paused', 'broken'], 'stopping');\n\n this.dcp4.instantiateAllConnections();\n\n // Do not enter is-screen-saver-active logic in the sandbox handle 'end' event handler.\n this.evaluator.pauseSandboxHandleEndHandler = true;\n\n if (forceTerminate)\n return this.abort();\n else\n {\n const slicesToReturn = [];\n for (const jm of this.jobManagerInventory)\n slicesToReturn.push(...jm.queuedSlices);\n\n const reason = `stopWork returning all non-finished slices that are not working`;\n this.returnSlices(slicesToReturn, reason);\n\n for (let k = 0; k < 3; k++)\n {\n await new Promise((resolve) => {\n // Count the slices that have been working or close-to-working but haven't submitted results yet.\n let activeSliceCount = 0;\n for (const jm of this.jobManagerInventory)\n activeSliceCount += jm.activeSlices.length;\n // When no active slices we're done.\n if (activeSliceCount === 0)\n resolve();\n // When no work can be completed we return all slices and leave.\n if (doNotWaitForWork())\n {\n this.returnAllSlices();\n resolve();\n }\n selectiveDebug() && console.debug(`StopWork: waiting for ${activeSliceCount} working slices to finish`, k);\n // Resolve and finish stopWork once all sandboxes have finished submitting their results.\n this.worker.on('result', () => {\n selectiveDebug() && console.debug(`StopWork: result handler, activeSliceCount ${activeSliceCount-1}`);\n if (--activeSliceCount === 0)\n {\n this.warning('All sandboxes empty, stopping worker and closing all connections');\n resolve();\n }\n });\n this.on('evalDown', () => {\n this.warning('Evaluator is down.', 'Force return all slices to scheduler, stopping worker and closing all connections.');\n this.returnAllSlices();\n resolve();\n });\n });\n }\n\n for (const jm of this.jobManagerInventory)\n this.safeEmit(jm.jobHandle, 'flush');\n\n if (selectiveDebug())\n {\n console.debug(`stopWork(${this.state.valueOf()}): After waiting for working slices to finish: workingSbxes: ${this.workingSandboxCount}, totalSbxes: ${this.sandboxInventory.length}, jobs: ${this.jobManagerInventory.length}`);\n this.jobManagerInventory.forEach((jm) => {\n console.debug('stopWork job', jm.identifier);\n console.debug(jm.countSliceStr('stopWork'));\n });\n }\n }\n\n return this.postStopShutdown();\n }\n\n /**\n * Purge all traces of the job.\n * @param {JobManager} jobManager\n */\n purgeJob (jobManager)\n {\n selectiveDebug() && console.debug(`Supervisor.purgeJob ${jobManager.identifier}.`);\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 !== jobManager.address);\n this.jobManagerInventory.delete(jobManager);\n this.moduleCache.removeJob(jobManager.address);\n this.dbg.cleanUpDeadJob(jobManager.address);\n jobManager.destroy();\n }\n\n // _Idx\n //\n // roundRobinSlices, makeJobSelectionCursor, handleSandboxWorkError, handleFailedSlice\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 *\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 based on the following ruleset:\n * 1) cursor = makeJobSelectionCursor(), then cursor.next() returns a slice chosen as follows.\n * 2) Let concurrency range from 1 to maxWorkingCores.\n * 3) For a given concurrency, let readyJobs be all jobs such that jobMan.workingSliceDensity < concurrency.\n * 4) Do an ascending sort of readyJobs wrt jobMan.emaTotalTime.\n * 5) Pick a slice from the longest job in readyJobs that doesn't have any executing slices.\n * 6) Alternately shift a slice from readySlices vs choose a slice from a random nearly finished job, and remove slice from readySlices.\n * 7) When there are no more almost finished jobs with slices, shift slices from readyJobs.\n * 8) Jobs which have slicePriority set by the task-distributor may have slices chosen ahead of the above algorithm.\n * 9) Jobs with a slicePriority closer to 1 are more likely to be chosen.\n * 10) After finishing concurrency at maxWorkingCores, cursor.next() returns null, so create a new cursor.\n * @returns {Promise<any>}\n */\n roundRobinSlices ()\n {\n //\n // Should we try to put all runSlice promises in an array and return Promise.all(runslice-promises) ?\n //\n try\n {\n /**\n * The amount of space available for the CPU-component of slices to run in sandboxes.\n * If space is 2.5 and there are 6 slices with density 0.4, and there are enough non-working usable\n * sandboxes, then all 6 slices will be scheduled to run.\n * @type {number}\n */\n const unusedCoreSpace = this.unusedCoreSpace;\n /**\n * The number of sandboxes not currently being used.\n * @type {number}\n */\n const unusedSandboxCount = this.unusedSandboxCount;\n /**\n * The amount of space available for the GPU-component of slices to run in sandboxes.\n * @type {number}\n */\n const unusedGPUSpace = this.unusedGPUSpace;\n if (unusedCoreSpace < common.doNotSchedule || this.roundRobinBarrier || unusedSandboxCount < 1)\n {\n selectiveDebug2() && console.debug('RRS: bail early space/barrier/unusedSlots', unusedCoreSpace, this.roundRobinBarrier, unusedSandboxCount);\n return;\n }\n // roundRobinBarrier is a barrier for the slice execution path.\n this.roundRobinBarrier = true;\n if (this.evaluator.down && this.evaluator.createSandboxRefCount > 0)\n return;\n selectiveDebug2() && console.debug('BarrierState:RRS:', this.fetchTaskBarrier, this.roundRobinBarrier);\n\n if (selectiveDebug2())\n {\n this.jobManagerInventory.forEach((jobMan) => {\n const { unassigned, ready, reserved, working, workdone, complete, failed, finished } = jobMan.dumpSlices ('RRS', false, false);\n console.debug(`RRS(${jobMan.identifier},${unusedSandboxCount},${unusedCoreSpace}): u/r/rsv/w/wd/c/f/fsh ${unassigned}/${ready}/${reserved}/${working}/${workdone}/${complete}/${failed}/${finished}`, jobMan.identifier, this.sandboxInventory.length);\n });\n }\n\n if ( false || selectiveDebug())\n {\n let totalReady = 0, totalReadyDensity = 0;\n for (const jobMan of this.jobManagerInventory)\n {\n const currentReady = jobMan.readySlices.length;\n const currentReadyDensity = jobMan.readySlices.length * jobMan.estimateDensity;\n totalReady += currentReady;\n totalReadyDensity += currentReadyDensity;\n console.debug(`RRS: job ${jobMan.identifier}, density ${jobMan.estimateDensity}, readySlices ${currentReady}, readyDensity ${currentReadyDensity}`);\n }\n console.debug(`RRS: space ${unusedCoreSpace}, unusedSandboxCount ${unusedSandboxCount}, totalReady ${totalReady}, totalReadyDensity ${totalReadyDensity}`);\n }\n\n /** @type {Slice[]} */\n const slices = [];\n /** @type {number} */\n let density = 0;\n /** @type {number} */\n let gpuDensity = 0;\n\n const isSpaceAvailable = (density) => {\n const result = density < unusedCoreSpace && slices.length < unusedSandboxCount;\n selectiveDebug2() && console.debug('RRS: isSpaceAvailable', density < unusedCoreSpace, slices.length < unusedSandboxCount);\n return result;\n }\n\n // When the cursor is almost done and RRS tries to schedule slices,\n // it makes sense to recreate the cursor once to ensure enough slices can be pulled from cursor.\n let recreateCursorCount = 0;\n\n while (isSpaceAvailable(density + common.schedulingSlop))\n {\n // Get existing cursor or create new one.\n if (!this.cursor)\n this.cursor = this.makeJobSelectionCursor();\n\n // Get the next slice, then check to see whether it can be used.\n const slice = this.cursor.next();\n if (!slice)\n {\n if (/*!okToSchedule ||*/ ++recreateCursorCount > 1)\n {\n this.cursor = null;\n break;\n }\n // Start a new cursor.\n this.cursor = this.makeJobSelectionCursor();\n continue;\n }\n let okToSchedule = true\n const job = slice.jobManager;\n density += job.estimateDensity;\n if (job.useGPU)\n {\n okToSchedule = canScheduleGPU(this.maxWorkingGPUs);\n if (okToSchedule)\n {\n gpuDensity += job.estimateGPUDensity;\n okToSchedule = (gpuDensity <= unusedGPUSpace);\n selectiveDebug2() && console.debug(`RRS: GPU scheduling(${okToSchedule},${this.workingSliceCount},${density.toFixed(7)},${unusedCoreSpace.toFixed(7)}): gpuDensity/gpuSpace ${gpuDensity.toFixed(7)}/${unusedGPUSpace.toFixed(7)}, jobGPUDensity/jobCPUDensity ${job.estimateGPUDensity.toFixed(7)}/${job.estimateDensity.toFixed(7)}`);\n }\n }\n if (okToSchedule && density <= unusedCoreSpace + common.schedulingSlop) // Ok, if it's only over by a little bit.\n slices.push(slice);\n else\n {\n slice.unReserve();\n density -= job.estimateDensity;\n if (job.useGPU)\n gpuDensity -= job.estimateGPUDensity;\n else\n this.cursor.push(slice); // If useGPU, then skip pulling a slice from job\n break;\n }\n selectiveDebug2() && console.debug('RRS: density/space/numSlices/unusedSlots/jobDensity', density, unusedCoreSpace, slices.length, unusedSandboxCount, job.estimateDensity);\n }\n\n selectiveSupEx() && density > 0 && console.debug(`roundRobinSlices(${this.workingSliceCount},${this.workingSliceDensity}): Found density ${density.toFixed(7)}/${unusedCoreSpace} with ${slices.length} slices:`, slices.map((slice) => slice.identifier), this.jobManagerInventory.map((jm) => `${jm.identifier}:${jm.estimateDensity.toFixed(7)}:${jm.emaSliceTime.toFixed(0)}`));\n\n // Execute the slices.\n if (slices.length > 0)\n {\n const lastSlice = slices.pop();\n for (const slice of slices)\n slice.jobManager.runSlice(slice);\n return lastSlice.jobManager.runSlice(lastSlice);\n }\n }\n finally\n {\n this.roundRobinBarrier = false;\n }\n }\n\n /**\n * @private\n * @callback cbNext\n * @returns {Slice}\n */\n /**\n * @private\n * @callback cbPush\n * @param {Slice} 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 based on the following ruleset:\n * 1) cursor = makeJobSelectionCursor(), then cursor.next() returns a slice chosen as follows.\n * 2) Let concurrency range from 1 to maxWorkingCores.\n * 3) For a given concurrency, let readyJobs be all jobs such that jobMan.workingSliceDensity < concurrency.\n * 4) Do an ascending sort of readyJobs wrt jobMan.emaTotalTime.\n * 5) Pick a slice from the longest job in readyJobs that doesn't have any executing slices.\n * 6) Alternately shift a slice from readySlices vs choose a slice from a random nearly finished job, and remove slice from readySlices.\n * 7) When there are no more almost finished jobs with slices, shift slices from readyJobs.\n * 8) Jobs which have slicePriority set by the task-distributor may have slices chosen ahead of the above algorithm.\n * 9) Jobs with a slicePriority closer to 1 are more likely to be chosen.\n * 10) After finishing concurrency at maxWorkingCores, cursor.next() returns null, so create a new cursor.\n *\n * A custom selection of jobs can be passed in via the argument jobManagers.\n *\n * @param {JobManager[]} [jobManagers]\n * @returns {{ next: cbNext, push: cbPush }}\n */\n makeJobSelectionCursor (jobManagers)\n {\n /* Variables in this scope function as state information for next() */\n /** @type {JobManager[]} */\n var candidateJobs; // The jobs available with slices ready to execute.\n /** @type {JobManager[]} */\n var readyJobs; // The jobs from which slices are selected for a given concurrency level.\n /** @type {JobManager[]} */\n var preferedJobs = []; // Those jobs in readyJobs with a slicePreference property.\n /** @type {JobManager[]} */\n var lowDensityJobs = []; // Jobs with density <= 0.6, will be scheduled again.\n /** @type {Slice[]} */\n var pendingSlices = [];\n /**\n * Upper bound of the sum of the working slices densities allowed for a given job.\n * type {number}\n **/\n var concurrency = 0;\n /** type {number} */\n var jobIdx = 0;\n /** @type {boolean} */\n var lowDensityPass = false;\n\n const that = this;\n if (!jobManagers)\n jobManagers = this.jobManagerInventory;\n\n const jobStateStr = (jobs) => {\n return jobs.map((jm) => `${jm.identifier} : ${jm.readySlices.length} : ${jm.workingSliceDensity} : ${Math.round(jm.emaTotalTime)}`);\n }\n const jobState = (hdr, jobs) => { console.debug(hdr, jobStateStr(jobs)); }\n\n /**\n * Populate readyJobs with jobs which are ready and have at least one slice which is ready,\n * and whose # of working slice density is less than concurrency. A reserved slice has a\n * finite lifetime and if exceeded, transition it back to ready.\n * @param {JobManager[]} jobManagers\n * @param {number} concurrency\n */\n function filterJobsAndCheckOldReservedSlices (jobManagers, concurrency) // eslint-disable-line no-shadow\n {\n candidateJobs = [], readyJobs = [];\n const fiveMinutesAgo = Date.now() - that.options.reservedSliceLifetime;\n for (const jobMan of jobManagers)\n {\n if (!jobMan.ready) continue;\n let readyCount = 0;\n for (const slice of jobMan.sliceInventory)\n {\n if (slice.isReady) readyCount++;\n else if (slice.isReserved && fiveMinutesAgo > slice.startTime)\n {\n slice.unReserve();\n readyCount++;\n }\n }\n if (readyCount > 0)\n {\n candidateJobs.push(jobMan);\n if (jobMan.workingSliceDensity < concurrency) readyJobs.push(jobMan);\n }\n }\n }\n\n function seed (concurrency) // eslint-disable-line no-shadow\n {\n /* Reset. */\n jobIdx = 0;\n\n /* Populate readyJobs with jobs which are ready and have at least one slice which is ready,\n and whose # of working slice density is less than concurrency. */\n filterJobsAndCheckOldReservedSlices(jobManagers, concurrency);\n // candidateJobs = jobManagers.filter((jobMan) => jobMan.readySlices.length > 0);\n // readyJobs = candidateJobs.filter((jobMan) => jobMan.workingSliceDensity < concurrency);\n\n if (!lowDensityPass && lowDensityJobs.length === 0)\n lowDensityJobs = jobManagers.filter((jm) => jm.estimateDensity > 0 && jm.estimateDensity <= 0.6);\n\n if (readyJobs.length > 1)\n {\n /* Asc sort by shortest average slice completion time. */\n const shortestSliceJobs = readyJobs.sort((a, b) => Math.round(a.emaTotalTime) - Math.round(b.emaTotalTime));\n const almostDoneIndices = shortestSliceJobs.filter((jm) => jm.almostDone).map((_, idx) => idx);\n readyJobs = [];\n\n /* Find longest job that isn't working. */\n for (let k = shortestSliceJobs.length - 1; k >= 0; k--)\n {\n const jobMan = shortestSliceJobs[k];\n if (jobMan.isNotWorking)\n {\n readyJobs.push(jobMan);\n shortestSliceJobs.splice(k, 1);\n break;\n }\n }\n\n /* Alternate the next shortest slice with a random almost done job. */\n if (almostDoneIndices.length > 0)\n {\n while (shortestSliceJobs.length > 0)\n {\n readyJobs.push(shortestSliceJobs.shift());\n if (almostDoneIndices.length < 1)\n break;\n else\n {\n const almostDoneIdx = almostDoneIndices[Math.floor(Math.random() * almostDoneIndices.length)];\n readyJobs.push(shortestSliceJobs[almostDoneIdx]);\n shortestSliceJobs.splice(almostDoneIdx, 1);\n }\n }\n }\n if (shortestSliceJobs.length > 0)\n readyJobs.push(...shortestSliceJobs);\n }\n /* Populate preferedJobs with jobs from readyJobs which also have a slicePreference set. */\n preferedJobs = candidateJobs.filter((jm) => jm.hasOwnProperty('slicePreference'));\n selectiveDebug2() && jobState(`makeJobSelectionCursor:seed(${concurrency}): readyJobs:`, readyJobs);\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 if (pendingSlices.length > 0)\n {\n const slice = pendingSlices.pop();\n return slice.markAsReserved();\n }\n\n if (concurrency === 0)\n seed(++concurrency);\n\n selectiveDebug2() && console.debug(`makeJobSelectionCursor(cc/idx/ready/working):next(${concurrency},${jobIdx},${readyJobs.length},${that.workingSliceDensity}): maxWorkingCores ${that.maxWorkingCores}: begin`);\n while (true)\n {\n if (jobIdx >= readyJobs.length)\n {\n if (++concurrency > that.maxWorkingCores)\n break;\n seed(concurrency);\n }\n\n if (readyJobs.length < 1)\n {\n if (candidateJobs.length < 1)\n break;\n continue; /* No ready jobs at current concurrency level. */\n }\n\n selectiveDebug2() && console.debug(`makeJobSelectionCursor:next(${concurrency},${jobIdx},${readyJobs.length},${that.workingSliceDensity}): before loop`);\n\n /* Schedule a prefered job slice based on random chance. */\n if (preferedJobs.length > 0)\n {\n let prioRan = Math.random();\n let list = preferedJobs.filter((jm) => jm['slicePreference'] >= prioRan);\n\n if (list.length > 0)\n {\n const jobMan = list[list.length * Math.random()];\n const slice = jobMan.reserveOneSlice();\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 (jobIdx < readyJobs.length)\n {\n const jobMan = readyJobs[jobIdx];\n const slice = jobMan.reserveOneSlice();\n if ( false || selectiveDebug2())\n {\n slice && console.debug(`makeJobSelectionCursor:next(${concurrency},${lowDensityPass},${jobIdx},${readyJobs.length},${that.workingSliceDensity}): found slice(slice:ready:working)`, `${slice.identifier} : ${slice.jobManager.readySlices.length} : ${slice.jobManager.workingSliceDensity}`);\n !slice && console.debug(`makeJobSelectionCursor:next(${concurrency},${lowDensityPass},${jobIdx},${readyJobs.length},${that.workingSliceDensity}): no slices ready for job`, jobMan.identifier);\n }\n jobIdx++;\n if (slice)\n return slice;\n }\n\n /*\n * We did not schedule a slice with current seed. We need to re-seed to look for newly-available work\n * and sandboxes, ratcheting up the concurrency (max # of each job running) until we find something.\n */\n selectiveDebug2() && console.debug(`makeJobSelectionCursor:next(${concurrency},${jobIdx},${readyJobs.length},${that.workingSliceDensity}): after loop`);\n }\n if (!lowDensityPass && lowDensityJobs.length > 0)\n {\n jobManagers = lowDensityJobs;\n concurrency = 0;\n lowDensityPass = true;\n return next();\n }\n selectiveDebug2() && console.debug(`makeJobSelectionCursor:next(${concurrency},${jobIdx},${readyJobs.length},${that.workingSliceDensity}): null`, lowDensityPass);\n return null; /* Did not find any more work that fits. */\n }\n function push (slice) { pendingSlices.push(slice); }\n\n return { next, push };\n }\n\n\n /**\n * Handle sandbox.work(...) errors.\n * @todo The orginal code from 2019 did not terminate sandbox when not SandboxError and Sandbox code didn't already terminate. Do we want to try that?\n * The old 2019 sandbox code terminated upon error in start, assign, resetState, describe, applyRequirements and work.\n * So maybe that 2019 terminate yoga was a bunch of hooie.\n * @param {Sandbox} sandbox\n * @param {Slice} slice\n * @param {Error} error\n * @returns {string}\n */\n handleSandboxWorkError (sandbox, slice, error)\n {\n if (debugBuild && !(slice.isWorking || slice.isWorkDone)) // Sanity. Exception should never fire.\n throw new Error(`handleSandboxWorkError: slice ${slice.identifier} must be WORKING.`);\n\n /** @type {boolean} */\n const isSandboxError = error instanceof SandboxError;\n /** @type {string} */\n let reason;\n const jobAddress = common.truncateAddress(slice.jobAddress);\n\n if (isSandboxError)\n reason = error['errorCode']\n else\n {\n // This error was unrelated to the work being done.\n reason = 'Slice has failed to complete execution';\n if (!error)\n error = new Error(`Slice ${slice.sliceNumber} in state ${slice.state} of job ${jobAddress} failed to complete execution`);\n }\n selectiveDebug() && console.debug('handleSandboxWorkError', slice.identifier, error);\n\n let errorString, onlyDisplayErrorString = true;\n if (error.name === 'EWORKREJECT')\n {\n reason = 'EWORKREJECT'; // The status.js processing does not have a case for 'EWORKREJECT'.\n errorString = !slice.hasBeenRejected\n ? `Slice rejected work: ${error.message}.`\n : `Slice rejected work twice; terminate job: ${error.message}.`\n error.stack = 'Sandbox was terminated by work.reject()';\n this.handleWorkReject(slice, error);\n }\n else\n {\n if (!this.evaluator.down)\n {\n /** Do we to be more selective when we retry a slice? */\n if (/*!isSandboxError ||*/ slice['useRetryLogic'])\n {\n slice['sandboxErrorCount'] = ( slice['sandboxErrorCount'] ?? 0) + 1;\n if (slice['sandboxErrorCount'] <= this.options.maxSliceRetries)\n slice.resetState(); // Try to reuse the slice.\n }\n }\n if (!slice.isReady)\n {\n selectiveDebug() && console.debug(`handleSandboxWorkError: returning slice ${slice.identifier}`);\n this.returnSlice(slice, reason)\n .finally (() => {\n this.handleFailedSlice(slice, error)\n });\n }\n\n switch (reason)\n {\n case 'ENOPROGRESS':\n errorString = 'No progress error in sandbox.';\n break;\n case 'ESLICETOOSLOW':\n errorString = 'Slice too slow error in sandbox.';\n break;\n case 'EPERM_ORIGIN':\n errorString = `Could not fetch data; origin not allowed: ${error.message}.`;\n break;\n case 'EFETCH':\n errorString = `Could not fetch data: ${error.message}.`;\n break;\n case 'EUNCAUGHT':\n onlyDisplayErrorString = false;\n errorString = `Uncaught error in sandbox: ${error.message}.`;\n break;\n default:\n onlyDisplayErrorString = false;\n errorString = `Slice failed in sandbox: ${error.message}.`;\n break;\n }\n }\n\n // Always terminate sandbox.\n this.returnSandbox(sandbox);\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,\n sliceNumber: slice.sliceNumber,\n sandbox: sandbox.id,\n jobName: sandbox.public ? sandbox.public.name : 'unnamed',\n };\n\n if (!displayMaxInfo && onlyDisplayErrorString)\n this.error(errorString, '', '', true);\n else\n {\n Object.entries(errorObject).forEach(([k,v]) => (errorString += `\\n ${k}: ${v}`));\n this.error(errorString, error, '', true);\n }\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 */\n handleFailedSlice (slice, error)\n {\n debugging('supervisor') && console.debug(`handleFailedSlice: ${slice.identifier}`, error);\n slice.collectResult(error, false /*success*/);\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.workerEmit( 'result', error);\n this.jobEmit(slice, 'result', error);\n }\n\n // _Idx\n //\n // returnSlices, returnSlice, emitProgressReport\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 slice candidates to check if they can be returned to the scheduler.\n * @param {string} reason - Reason for the return: 'ENOPROGRESS', 'EUNCAUGHT', 'ESLICETOOSLOW', 'EPERM_ORIGIN', 'EFETCH', 'unknown'.\n * @returns {Promise<*>} - Response from the scheduler.\n */\n returnSlices (slices, reason)\n {\n /** @param {Slice[]} verifiedSlices */\n const compressPayload = (verifiedSlices) => {\n assert(verifiedSlices?.length > 0);\n if (verifiedSlices.length > 1)\n return {\n worker: this.workerId,\n slices: common.constructReturnSliceBuckets(verifiedSlices, reason),\n };\n return verifiedSlices[0].getReturnMessagePayload(this.workerId, reason);\n }\n\n if (!slices || !slices.length)\n return Promise.resolve();\n\n debugging('supervisor') && console.debug(`Supervisor.returnSlices(${this.state}): Returning slices`, slices.map(slice => slice.identifier));\n\n // Only return those slices which still exist in their respective jobManagers sliceInventory .\n const verifiedSlices = slices.filter((slice) => slice.jobManager.removeSlice(slice));\n if (verifiedSlices.length > 0)\n {\n selectiveSupEx() && console.debug('Supervisor.returnSlices: Returning slices', verifiedSlices.map(slice => slice.identifier));\n return this.dcp4.sliceReturn(compressPayload(verifiedSlices), slices, reason);\n }\n return Promise.resolve();\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(...) .\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 - Reason for the return: ''ENOPROGRESS', 'EUNCAUGHT', 'ESLICETOOSLOW', 'EPERM_ORIGIN', 'EFETCH', 'unknown'.\n * @returns {Promise<*>} - Response from the scheduler.\n */\n returnSlice (slice, reason) { return this.returnSlices([ slice ], reason); }\n\n /**\n * Send beacon to status.js for 'progress' and sliceStatus.scheduled.\n *\n * Run in an interval created in the ctor.\n * @returns {void|Promise<*>}\n */\n emitProgressReport ()\n {\n const readySlices = [], workingSlices = [];\n this.jobManagerInventory.forEach((jobManager) => {\n readySlices.push(...jobManager.readySlices);\n workingSlices.push(...jobManager.workingSlices);\n });\n /** @type {SliceObj[]} */\n const slices = common.constructSliceBuckets( readySlices, sliceStatus.scheduled );\n common.constructSliceBuckets( workingSlices, 'progress', slices );\n\n debugging('supervisor') && console.debug('emitProgressReport:', stringify(slices));\n\n if (slices.length > 0)\n {\n const payload = { worker: this.workerId, slices };\n return this.dcp4.safeRSStatus(payload, 'Failed to emit progress report');\n }\n }\n\n // _Idx\n //\n // jobQuanta, repoMan, predictLoad(viz., clairvoyance), waitUntilWorkIsReady, generateWorkerComputeGroups\n //\n\n /**\n * For a given job, the scheduler stores an EMA approximation of slice completion time.\n * However, each worker also tracks the same information and the ratio of local-info to\n * scheduler-info (viz., global-info) is returned by this.jobQuanta so we can tell the\n * task distributor how much work to return from fetchTask so that the work actually takes\n * 5 minutes to complete when using all the worker sandboxes.\n * @returns {Object<string, number>}\n */\n jobQuanta ()\n {\n //\n // Prevent wild swings of this.defaultQuanta, which is roughly the ratio of\n // local_worker_slice_time / jobPerfData_measured_slice_time.\n // We limit this ratio to be between 1/8th and 8.\n const minQuanta = 0.125, maxQuanta = 8.0;\n //\n // Because there will be slgiht differences between local_worker_slice_time and\n // jobPerfData_measured_slice_time even when the worker is the only worker hooked\n // up to the DCP scheduler, we prove a little rounding. The rounding is 1/32 buckets.\n const discreteIncrement = 0.03125, discreteIncrementInverse = 32;\n\n /** @type {Object<string, number>} */\n const quanta = { 0: 1 };\n let averageLocalTime = 0, averageGlobalTime = 0;\n for (const jobMan of this.jobManagerInventory)\n {\n if (jobMan.emaSliceTime > 0 && jobMan.globalTime > 0)\n {\n quanta[jobMan.address] = jobMan.emaSliceTime;\n /** XXXpfr @todo Should we be using TotalTime here? */\n averageLocalTime += jobMan.emaSliceTime;\n averageGlobalTime += jobMan.globalTime;\n selectiveDebug2() && console.debug('jobQuanta: job state', this.dbg.sliceSandboxStr, `l-density/g-density, ${jobMan.estimateDensity}/${jobMan.metrics?.sliceCPUDensity}`, `local/global, ${jobMan.emaSliceTime}/${jobMan.globalTime}`);\n }\n }\n\n if (averageLocalTime && averageGlobalTime)\n {\n /** @todo XXXpfr Add 1 stddev? */\n // alpha=0.1 gives an effective period of 19\n const alpha = 0.1;\n this.localTime = nextEma(this.localTime, averageLocalTime, alpha);\n this.globalTime = nextEma(this.globalTime, averageGlobalTime, alpha);\n this.defaultQuanta = this.localTime / this.globalTime;\n\n // Discretize by discreteIncrement increments.\n this.defaultQuanta = (this.defaultQuanta > 1\n ? Math.floor(discreteIncrementInverse * this.defaultQuanta)\n : Math.ceil(discreteIncrementInverse * this.defaultQuanta)) * discreteIncrement;\n\n // Enforce reasonable cap and floor to keep things from getting too crazy.\n this.defaultQuanta = Math.min(Math.max(this.defaultQuanta, minQuanta), maxQuanta);\n\n // Fake jobAddress '0' to represent unknown jobs.\n quanta['0'] = this.defaultQuanta;\n }\n else\n this.defaultQuanta = 1.0;\n\n selectiveDebug() && console.debug(`jobQuanta: defaultQuanta ${quanta['0']}, this.localTime ${this.localTime}/${averageLocalTime}, this.globalTime ${this.globalTime}/${averageGlobalTime}, quanta:`, quanta);\n if (common.debugQuanta())\n {\n console.debug('localRawData:', this.dbg.localRawData);\n console.debug('localData:', this.dbg.localData);\n console.debug('globalData:', this.dbg.globalData);\n }\n return quanta;\n }\n\n /**\n * @todo XXXpfr Should we not schedule long slices to a worker with too low defaultQuanta?\n *\n * When the estimated time to completion of all work is more than\n * repoManMultiplier * targetTaskDuration * this.maxWorkingCores,\n * return slices until the excess is removed.\n * Be fair. Round-robin over all jobs until excess is eliminated.\n * Kill the long jobs 1st.\n */\n repoMan()\n {\n const threshold = this.options.repoManMultiplier * this.options.targetTaskDuration * this.maxWorkingCores;\n const workRemaining = this.workRemaining;\n let excess = workRemaining - threshold;\n selectiveDebug() && console.debug(`repoMan: excess ${excess}, workerRemaining ${workRemaining}, threshold ${threshold}`);\n if (excess > 0)\n {\n const slices = [];\n /** @param {JobManager[]} jmi */\n const returnFrom = (jmi) => {\n while (true)\n {\n const _excess = excess;\n for (const jobMan of jmi)\n {\n const _readySlices = jobMan.readySlices;\n if (_readySlices.length > 0)\n {\n const slice = _readySlices[0];\n slice.repoMan(); // Mark as FINISHED\n slices.push(slice);\n excess -= jobMan.adjSliceTime;\n if (excess <= 0)\n break;\n }\n }\n if (_excess === excess || excess <= 0)\n break;\n }\n }\n // Be fair. Round-robin over all jobs until excess is eliminated.\n // Except the long jobs are killed 1st.\n const longJobs = this.jobManagerInventory.filter((jobMan) => jobMan.emaSliceTime >= this.options.targetTaskDuration);\n if (longJobs.length > 0)\n returnFrom(longJobs);\n if (excess > 0)\n returnFrom(this.jobManagerInventory);\n selectiveDebug() && (slices.length > 0) && console.debug(`repoMan: excess ${excess}, workerRemaining ${workRemaining}, threshold ${threshold}, returned-slice-count ${slices.length}`);\n this.returnSlices(slices, 'repoMan');\n }\n }\n\n /**\n * Predict the total reduction in density of working sandboxes timeSpanMs from now.\n * This function is called right before fetchTask, in order to calculate how much space is available.\n * @returns {{ queued: Slice[], working: number }}\n */\n predictLoad()\n {\n const timeSpanMs = this.options.prefetchInterval\n const queued = [];\n let working = 0;\n for (const jobMan of this.jobManagerInventory)\n {\n const { queued: jmQueued, working: jmWorking } = jobMan.predictLoad(timeSpanMs);\n queued.push(...jmQueued);\n // Optimize to short-circuit when queued > 1, because we won't call fetchWork in that case.\n if (queued.length > 1)\n break;\n working += jmWorking;\n }\n selectiveDebug() && console.debug(`Supervisor.predictLoad: queued ${queued.length}/${this.queuedSlices.length}, working ${working}/${this.workingSlices.length}`)\n return { queued, working };\n }\n\n /**\n * On the first call to fetchTask\n * or when the last call to fetchTask found nothing,\n * or when there are no ready slices,\n * wait until at least 1 job is ready with at least 1 ready slice.\n * @param {Array<Promise<any>>} jobManagerPromises\n * @returns {Promise<any>}\n */\n waitUntilWorkIsReady (jobManagerPromises)\n {\n if (this.waitForWork)\n {\n debugging('supervisor') && console.debug(`waitUntilWorkIsReady: promise count ${jobManagerPromises?.length}`);\n this.waitForWork = false;\n // Promise.any is supported in Node 15, Chrome 85, Edge 85, Firefox 79, Safari 14, Opera 71.\n // It was implemented in node and browsers in 2nd half of 2020, so there's a good chance many\n // customers will not have browsers that support it. And currently (Jan. 2023) DCP uses node 14.\n return Promise_any(jobManagerPromises);\n }\n // Flush microtask queue\n return a$sleepMs(0);\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 return supShared.generateWorkerComputeGroups(this, this.dcp4.taskDistributor);\n }\n\n // _Idx\n //\n // availableSandboxSpace, fetchTask, addTaskToWorkload, fetchFromTD, clearUnusedJobManagersAndModuleCache\n //\n\n /**\n * Returns the number of unused sandbox slots to fill -- sent to fetchTask.\n * @param {Slice[]} queued\n * @param {number} working\n * @returns {number}\n */\n availableSandboxSpace (queued, working)\n {\n // If we find more than 1 queued slices, bail early.\n if (queued.length > 1)\n return 0; // We have more than 1 ready slices, no need to fetch.\n\n let longSliceCount = 0;\n if (queued.length < 1)\n this.waitForWork = true; // There are no ready slices.\n else if (queued[0].isLong)\n longSliceCount = 1;\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 roughly this.maxWorkingCores * 5-minutes.\n // However, there can only be this.maxWorkingCores # of long slices on a worker,\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 or is an estimation slice.)\n\n const numCores = this.maxWorkingCores - working - longSliceCount;\n selectiveDebug2() && console.debug('availableSandboxSpace', numCores, working, longSliceCount);\n return numCores;\n }\n\n /**\n * Ask the scheduler (task distributor) for work (Rq).\n * @param {object[]} [jobs=[]]\n * @returns {Promise<*>}\n */\n async fetchTask (jobs = [])\n {\n if (!this.isReady)\n return;\n\n const now = Date.now();\n const { queued, working } = this.predictLoad();\n const unusedFutureCoreSpace = this.maxWorkingCores - working;\n if (unusedFutureCoreSpace < common.doNotSchedule)\n {\n debugging('supervisor') && console.debug('fetchTask: There are no unused sandbox slots.', now - this.lastTime);\n return;\n }\n \n // Record fetch start time.\n this.fetchTaskStarted = now;\n\n // We check for pruning about every 25 seconds, or when must prune level is reached.\n if (this.sandboxInventory.length > this.options.mustPruneSandboxLevel\n || now > this.lastPrune + this.options.pruneFrequency)\n {\n this.lastPrune = now;\n this.pruneSandboxes();\n }\n\n // Every 60 seconds check to see if the estimated time to completion of all work is more than\n // repoManMultiplier * this.targetTaskDuration() * this.maxWorkingCores,\n // and then return slices until the excess is removed.\n // Be fair. Round-robin over all jobs until excess is eliminated. Kill the long jobs 1st.\n if (now > this.lastRepoMan + this.options.repoManFrequency)\n {\n this.lastRepoMan = now;\n this.repoMan();\n }\n\n // There are 2 barriers wrt fetchTask,\n // 1) fetchTaskBarrier is a barrier for the task fetching from task distributor path.\n // 2) roundRobinBarrier is a barrier for the slice execution path.\n\n try\n {\n const cpuSpaceToFill = this.availableSandboxSpace(queued, working);\n selectiveDebug2() && console.debug('Supervisor.fetchTask', cpuSpaceToFill, queued.length, working);\n if (cpuSpaceToFill < 1)\n {\n debugging('supervisor') && console.debug('Supervisor.fetchTask: Sufficient slices exist, so start executing.', now - this.lastTime, cpuSpaceToFill, queued.length, working);\n return this.roundRobinSlices();\n }\n selectiveDebug2() && console.debug('fetchTask begin q/w/slots/space/future-space', queued.length, working, this.unusedSandboxCount, this.unusedCoreSpace, unusedFutureCoreSpace);\n\n if (this.fetchTaskBarrier)\n return;\n // fetchTaskBarrier is a barrier for the task fetching from task distributor path.\n this.fetchTaskBarrier = true;\n\n /* @todo XXXpfr Think about how to do targetLoad.longSlices better.\n * Ideas:\n * 1) While branchy Javascript is CPU bound, that doesn't mean hyperthreading isn't useful.\n * When a branch is mispredicted the whole CPU instruction pipeline is flushed, which is\n * a huge perf hit and while waiting to fill the pipeline again, a hyperthread can get\n * a whole bunch of work done.\n * 2) Setting the Sup2.cores.cpu to #lCores is probably too much, but I've had great success with\n * Sup2.cores.cpu = dcpConfig.supervisor.tuning.coreRatio.cpu * #lCores\n * Which is very close to optimal throughput of work done.\n * 3) When the scheduler is 1/2 very long slices, the short slices will tend to get starved.\n * The config property dcpConfig.scheduler.preventSliceStarvation when set to true (default false)\n * will always leave one vCore open for short slices in every worker. In the future, I want the\n * scheduler to detect short slice starvation and dynamical turn preventSliceStarvation on until\n * short slice starvation is alleviated and then turn it back off.\n * 4) maxSandboxes is currently set to\n * factor * Sup2.cores.cpu\n * where 1.2 <= factor <= 1.5 depending upon how many lCores a worker has. Where I assume that a\n * machine with a large number of lCores has sufficient memory to handle a bigger factor.\n * The factor boundaries can be adjusted in dcpConfig.supervisor, but I intend to also allow them\n * to be overridden at the dcpConfig.worker level, so if somebody has a 32 lCore machine with\n * 8GB of RAM they can adjust factor to be closer to 1. Ideally we could adjust the factor\n * boundaries at the job/CG level.\n */\n\n const request = {\n supervisor: this.version,\n numCores: cpuSpaceToFill, /** @deprecated This is for legacy schedulers. */\n numGPUs: this.maxWorkingGPUs, /** @deprecated This is for legacy schedulers. */\n targetLoad: { cpu: cpuSpaceToFill, gpu: this.maxWorkingGPUs, longSlices: Math.floor(cpuSpaceToFill) },\n coreStats: this.options.getStatisticsCPU(),\n jobQuanta: this.jobQuanta(),\n capabilities: this.capabilities,\n paymentAddress: this.options.paymentAddress,\n jobAddresses: jobs.concat(this.options.jobAddresses || []), // When set, only fetches slices for these jobs.\n workerComputeGroups: this.generateWorkerComputeGroups(),\n 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(this.options.leavePublicGroup))\n request.workerComputeGroups.push(constants.computeGroups.public);\n\n debugging('supervisor') && console.debug('fetchTask is calling fetchFromTD', Date.now() - this.lastTime);\n\n // Call Task Distributor and handle response with this.addTaskToWorkload.\n return this.fetchFromTD(request, (response) => this.addTaskToWorkload(request, response));\n }\n catch (error)\n {\n this.fetchTaskBarrier = false;\n this.error('Supervisor.fetchTask failed!', error);\n }\n }\n\n /**\n * Callback for fetchFromTD.\n * @param {object} request\n * @param {object} response\n */\n async addTaskToWorkload (request, response)\n {\n const constructFetchHandle = (size, jobs, slices) => {\n return { \n fetchStart: this.fetchTaskStarted,\n fetchEnd: Date.now(),\n fetchSize: size,\n jobs,\n slices,\n };\n };\n\n try\n {\n /** @type {TDPayload} */\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 this.error(`Unable to request task from scheduler; will try again on a new connection: payload ${stringify(payload)}`);\n return;\n }\n\n if (!payload.body?.newJobs) // No slices found.\n {\n // Reset first fetch logic.\n this.waitForWork = true;\n /**\n * The 'fetch' event fires when the stask distributor found no work.\n * @link https://gitlab.com/Distributed-Compute-Protocol/dcp-docs-wes/-/blob/wip/worker/worker-events.md\n * @event Supervisor#fetch\n */\n this.workerEmit('fetch', constructFetchHandle(0, {}, {}));\n // There may be an extra slice to process.\n debugging('supervisor') && console.debug('Task distributor found no slices...');\n return this.roundRobinSlices();\n }\n\n /** @todo XXXpfr At this poin the line #'s are short by 42 -- figure out why. */\n\n /*\n * payload: { TDPayload }\n * TDPayload: { owner: Address, signature: Signature, auth: Auth, body: Body };\n * Auth: { workerId: string, authSlices: Object<string, SliceMessage[]>, schedulerId: { address: Address }, jobCommissions: Object<string, { rate: number, account: number }> }\n * Body: { newJobs: Object<string, object>, task: Object<string, SliceMessage[]>, computeGroupJobs: Object<string, string[]>, computeGroupOrigins: Object<string, Object<string, string[]>>, schedulerConfig: {{ targetTaskDuration: number }} }\n *\n * NOTE: authorizationMessage has type AuthMessage\n */\n\n const { body, ...authorizationMessage } = payload;\n const { newJobs, task, schedulerConfig } = body;\n const newJobKeys = Object.keys(newJobs);\n const jobCount = newJobKeys.length;\n\n let jobSliceMap = task;\n if (jobSliceMap.length) /** @deprecated Task came from legacy scheduler */\n // @ts-ignore\n jobSliceMap = toJobMap(task, sliceMsg => sliceMsg);\n\n if (schedulerConfig) // Otherwise the default is 300 seconds.\n this.options.targetTaskDuration = schedulerConfig.targetTaskDuration;\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 {\n // \"fetchTask:\" because that should make sense to somebody that doesn't know the internals of Supervisor.\n this.error(\"fetchTask: Worker received slices it shouldn't have; rejecting the work and stopping.\");\n this.stopWork(true);\n return;\n }\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(newJobs);\n\n /** @todo XXXpfr Figure out how not to construct this every time. */\n this.jobMap = {};\n this.jobManagerInventory.forEach(jobManager => {\n this.jobMap[jobManager.address] = jobManager;\n });\n\n selectiveDebug2() && console.debug(`addTaskToWorkload(${Date.now() - this.lastTime}): newJobs ${common.truncateAddress(newJobKeys)}, jobSliceMap ${common.compressJobMap(jobSliceMap, (s) => s.sliceNumber)}`);\n\n let sliceCount = 0;\n /** @type {Array<Promise<*>>} */\n const jobManagerPromises = [], jobs = {}, slices = {};\n // Populate the job managers with slices, creating new job managers when necessary.\n // Set up discrete job ring buffer.\n for (const [jobAddress, jobMessage] of Object.entries(newJobs))\n {\n /** @type {JobManager} */\n let jobManager;\n const sliceMessages = jobSliceMap[jobAddress];\n sliceCount += sliceMessages.length;\n\n if (this.jobMap.hasOwnProperty(jobAddress))\n {\n jobManager = this.jobMap[jobAddress];\n jobManager.update(jobMessage, sliceMessages, authorizationMessage);\n }\n else\n {\n // Add the slice messages to the job manager ctor, so that slice construction is after job manager is ready.\n jobManager = new JobManager(this, jobMessage, sliceMessages, authorizationMessage);\n this.jobMap[jobAddress] = jobManager;\n this.jobManagerInventory.push(jobManager);\n\n // Populate the ring buffer based on job's discrete property.\n if (jobMessage.requirements.discrete && this.ringBufferofJobs.find(address => address === jobAddress) === undefined)\n this.ringBufferofJobs.push(jobAddress);\n }\n jobs[jobAddress] = jobManager.jobHandle;\n slices[jobAddress] = task[jobAddress].length;\n\n jobManagerPromises.push(jobManager.jobPromise);\n }\n\n const payloadLength = kvin.stringify(payload).length; /** @TODO - fix per DCP-3750 */\n /**\n * The 'fetch' event fires when the supervisor has found work from the task distributor.\n * @link https://gitlab.com/Distributed-Compute-Protocol/dcp-docs-wes/-/blob/wip/worker/worker-events.md\n * @event Supervisor#fetch\n */\n this.workerEmit('fetch', constructFetchHandle(payloadLength, jobs, slices));\n\n const compressTask = () => { return common.compressJobMap(authorizationMessage.auth.authSlices); }\n selectiveSupEx() && console.debug(`addTaskToWorkload(${Date.now() - this.lastTime}): task: ${sliceCount}/${request.targetLoad.cpu}/${this.maxWorkingCores}, jobs: ${jobCount}, authSlices: ${compressTask()}, conversion:`, request.jobQuanta);\n\n // On the first call to fetchTask,\n // or when the last call to fetchTask found nothing,\n // or when there are no ready slices,\n // wait until at least 1 job with 1 slice is ready.\n await this.waitUntilWorkIsReady(jobManagerPromises);\n\n debugging('supervisor') && console.debug('addTaskToWorkload: Before calling roundRobinSlices; job states', this.jobManagerInventory.map((jm) => jm.identifier));\n\n // Start working on the new slices.\n return dcp_timers.setImmediate(() => this.roundRobinSlices());\n }\n catch (error)\n {\n this.workerEmit('fetch', error);\n this.error('Supervisor.fetchTask failed!', error);\n }\n finally\n {\n this.fetchTaskBarrier = false;\n }\n }\n\n /**\n * @private\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<any>}\n */\n async fetchFromTD (request, addTaskToWorkload)\n {\n selectiveDebug2() && console.debug('fetchFromTD begin; BarrierState:', this.fetchTaskBarrier, this.roundRobinBarrier);\n // Fetch a new task if we have insufficient slices queued, then start workers\n if (!this.fetchTaskBarrier)\n throw new Error('fetchTaskBarrier must be set when entering fetchFromTD.');\n\n this.dcp4.instantiateAllConnections();\n\n let fetchTimeout = dcp_timers.setTimeout(() => {\n this.fetchTaskBarrier = false;\n this.warning('Fetch exceeded timeout, will reconnect at next watchdog interval');\n this.dcp4.resetConnection('taskDistributor').catch(error => {\n this.error('Failed to close task-distributor connection', error);\n });\n this.dcp4.resetConnection('resultSubmitter').catch(error => {\n this.error('Failed to close result-submitter connection', error);\n });\n this.dcp4.instantiateAllConnections();\n }, 3 * 60 * 1000); // Max out at 3 minutes to fetch.\n // Allow workers and localExec to exit.\n fetchTimeout.unref();\n\n const finalize = () => {\n this.fetchTaskBarrier = false;\n if (fetchTimeout)\n dcp_timers.clearTimeout(fetchTimeout);\n fetchTimeout = null;\n }\n\n // Ensure result submitter and task distributor connections before fetching tasks.\n try\n {\n await Promise.all([\n this.dcp4.taskDistributor.keepalive(),\n this.dcp4.resultSubmitter.keepalive(),\n ]);\n }\n catch (error)\n {\n selectiveDebug() && console.debug('fetchTaskFromTD: Keep slices failed', error);\n this.warning('Failed to connect to result submitter, refusing to fetch slices.', 'Will try again at next fetch cycle.');\n this.dcp4.resetConnection('taskDistributor').catch(e => {\n this.error('Failed to close task-distributor connection', e);\n });\n this.dcp4.resetConnection('resultSubmitter').catch(e => {\n this.error('Failed to close result-submitter connection', e);\n });\n return finalize();\n }\n\n if (!this.dcp4.taskDistributor)\n {\n const msg = 'Unable to request task from scheduler; no connection to task distributor';\n this.warning(msg);\n this.workerEmit('fetch', new Error(msg));\n return finalize();\n }\n \n // The 'beforeFetch' event allows the user to cancel the requestTask request.\n let canceled = false;\n /**\n * The 'beforeFetch' event fires before the request is sent to requestTask in task distributor.\n * @link https://gitlab.com/Distributed-Compute-Protocol/dcp-docs-wes/-/blob/wip/worker/worker-events.md\n * @event Supervisor#beforeFetch\n */\n this.workerEmit('beforeFetch', () => { canceled = true; })\n selectiveDebug() && canceled && console.debug('User canceled the fetch task.');\n if (canceled)\n return finalize()\n\n return this.dcp4.taskDistributor.request('requestTask', request)\n .then((response) => {\n addTaskToWorkload(response);\n // Success! Restore this.dcp4.taskDistributor delay to retryMinSleepMs (currently 32ms.)\n // Is there a better way to reset than explicit calls?\n this.delayManager.resetEBO('taskDistributor');\n return response;\n })\n .catch((error) => {\n this.workerEmit('fetch', error);\n this.error('Unable to request task from scheduler. Will try again on a new connection.', error);\n this.dcp4.resetConnection('taskDistributor');\n })\n .finally(() => {\n return finalize();\n });\n }\n\n /**\n * Remove all unreferenced jobs in this.jobManagerInventory and this.moduleCache.\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<string, number[]>} newJobMap - Jobs that should not be removed from this.jobManagerInventory and this.moduleCache.\n */\n clearUnusedJobManagersAndModuleCache (newJobMap)\n {\n const emptyJobs = [];\n for (const jobMan of this.jobManagerInventory) // Grab oldest 1st\n {\n if (!newJobMap[jobMan.address])\n {\n let isEmpty = true;\n for (const slice of jobMan.sliceInventory)\n if (slice.isQueuedOrActive)\n {\n isEmpty = false;\n break;\n }\n if (isEmpty)\n {\n // Walk through whole list to purge empty jobs with no assigned sandboxes to save.\n if (jobMan.assignedSandboxes.length < 1)\n this.purgeJob(jobMan);\n else\n emptyJobs.push(jobMan)\n }\n }\n }\n let deleteCount = this.jobManagerInventory.length - this.options.cachedJobsThreshold;\n if (deleteCount > 0)\n {\n selectiveDebug() && console.debug(`Supervisor.clearUnusedJobManagersAndModuleCache: deleteCount ${deleteCount}/${this.jobManagerInventory.length}/${this.options.cachedJobsThreshold}.`);\n for (const jobMan of emptyJobs) // Grab oldest 1st\n {\n this.purgeJob(jobMan);\n if (--deleteCount < 1)\n break;\n }\n }\n }\n\n // _Idx\n //\n // createSandbox, returnSandbox, hookUpSandboxListeners, pruneSandboxes\n //\n\n /**\n * Automatically handle when the evaluator is down.\n *\n * With the screensaver worker, when the screensaver goes down, so does the evaluator.\n * And when the screensaver starts running again, so does the evaluator. The evaluator\n * may be stopped and started again with sa worker running, and have good behavior.\n * However, browser workers cannot have their evaluators stopped without also stopping\n * the worker (otherwise file-a-bug...)\n *\n * @param {boolean} [throwError=false]\n * @returns {Promise<Sandbox>}\n */\n async createSandbox (throwError = false)\n {\n selectiveDebug2() && console.debug('createSandbox', this.sandboxInventory.length, Date.now() - this.lastTime);\n // See if there are any READY_FOR_ASSIGN sandboxes (viz., sandbox.isReadyForAssign is true.)\n // If the evaluator just came back up (while worker is still running) there should not be any non-assigned sandboxes.\n // We're only considering sa worker (e.g. screensaver worker), because browser workers cannot stop the\n // evaluator w/o stopping the worker (I think -- if not true, file-a-bug.)\n if (this.sandboxInventory.length > 0 && this.sandboxInventory[0].isReadyForAssign)\n {\n selectiveDebug2() && console.debug(`Supervisor.createSandbox: Found ready-for-assign sandbox ${this.sandboxInventory[0].identifier}`);\n return this.sandboxInventory.shift();\n }\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 try\n {\n this.evaluator.createSandboxRefCount++;\n\n let retry = 0;\n while (true)\n {\n let sandbox;\n try\n {\n sandbox = new Sandbox(this, { ...this.options.sandboxOptions });\n selectiveDebug2() && console.debug(`Supervisor.createSandbox(${sandbox.id}): Calling sandbox.start: ${this.evaluator.createSandboxRefCount}, eval-down ${this.evaluator.down}`);\n this.hookUpSandboxListeners(sandbox);\n await sandbox.start();\n if (!this.capabilities)\n this.checkCapabilities(sandbox);\n if (this.evaluator.reallyDown)\n {\n this.evaluator.reallyDown = false;\n selectiveDebug() && console.debug('Supervisor.createSandbox: Evaluator is up again.', this.evaluator.createSandboxRefCount);\n this.jobManagerInventory.forEach((jobManager) => jobManager.resetSlices('createSandbox'));\n }\n return sandbox;\n }\n catch (error)\n {\n if (throwError)\n throw error;\n selectiveDebug() && console.debug(`Supervisor.createSandbox: Failed to start sandbox ${sandbox.identifier}`, this.evaluator.createSandboxRefCount, this.evaluator.down, error.message);\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\n if (throwError)\n throw error;\n\n // The evaluator may be down or shutting down, keep retrying.\n if ((retry % 60) === 0)\n this.warning('Failed to start a sandbox; will keep retrying; screensaver worker or evaluator may be down...');\n await a$sleepMs(1000 * Math.min(5, ++retry));\n }\n }\n }\n finally\n {\n this.evaluator.createSandboxRefCount--;\n }\n }\n\n /**\n * Remove sandbox from inventory and terminate.\n * @param {Sandbox} sandbox\n */\n returnSandbox (sandbox)\n {\n // If sandbox is not in this.sandboxInventory then sandbox must already be terminated\n // <==> this.sandboxInventory.includes(sandbox) || sandbox.isTerminated().\n selectiveDebug2() && console.debug(`returnSandbox: ${sandbox.identifier}`);\n if (common.removeElement(this.sandboxInventory, sandbox))\n sandbox.terminate(false);\n else\n {\n // If sandbox is not in this.sandboxInventory then sandbox must already be terminated\n if (common.displayMaxDiagInfo() && !sandbox.isTerminated) // Design assumption.\n throw new Error(`returnSandbox: Sandbox ${sandbox.identifier} has already been removed.`);\n }\n }\n\n /**\n * For a given sandbox, hook up all the Sandbox listeners.\n * @param {Sandbox} sandbox\n */\n hookUpSandboxListeners (sandbox)\n {\n sandbox.addListener('start', () => {\n if (!sandbox.slice) return;\n const payload = sandbox.slice.getMessagePayload(this.workerId, 'begin');\n return this.dcp4.safeRSStatus(payload, `Failed to send 'begin' status for slice ${sandbox.slice.identifier}`);\n });\n\n const that = this;\n // Sandbox error handler.\n sandbox.on('sandboxError', function Supervisor$sandboxError(error) {\n selectiveDebug() && console.debug(`Sandbox ${sandbox.identifier} sandboxError-handler; error while executing work function`, error);\n const slice = sandbox.slice;\n if (!slice?.isWorking) // Sanity -- warning should never fire.\n this.warning(`handleSandboxError: slice ${slice?.identifier} must be WORKING.`);\n if (slice)\n slice['useRetryLogic'] = true;\n that.returnSandbox(sandbox);\n });\n\n // Sandbox complete handler.\n // When any sandbox completes, go through the Supervisor.fetchTask protocol.\n sandbox.addListener('complete', () => {\n // Try not to call fetchTask unless there's something there.\n selectiveDebug2() && console.debug('Sandbox complete listener', this.fetchTaskBarrier, this.roundRobinBarrier, this.unusedSandboxCount, Date.now() - this.lastTime);\n if (!this.fetchTaskBarrier)\n this.fetchTask();\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.sandboxHandle.on('end', async () => {\n if (this.sandboxInventory.length > 0 && !this.evaluator.pauseSandboxHandleEndHandler)\n {\n selectiveDebug() && console.debug(`hookUpSandboxListeners: Sandbox \"${sandbox.identifier}\" terminated handler`, this.sandboxInventory.length, Date.now() - this.lastTime);\n\n // Does there exist a non-terminated sandbox?\n let allSandboxesTerminated = true;\n for (const sbx of this.sandboxInventory)\n if (!sbx.isTerminated)\n {\n allSandboxesTerminated = false;\n break;\n }\n\n if (allSandboxesTerminated && !this.evaluator.downInterlock)\n {\n //\n // When we get here, all sandboxes have been terminated.\n //\n this.evaluator.downInterlock = true;\n selectiveDebug() && console.debug('hookUpSandboxListeners: Try to create 1 sandbox in the sandbox-terminated-handler...', sandbox.identifier);\n await this.createSandbox(true /*throwError*/)\n .then((sbx) => {\n this.evaluator.reallyDown = false;\n // This is the only place where non-assigned sandboxes are added to this.sandboxInventory.\n this.sandboxInventory.unshift(sbx);\n selectiveDebug() && console.debug('Sandbox terminate handler was able to create new sandbox', sandbox.identifier);\n })\n .catch(() => {\n //\n // Since all sandboxes have been terminated, if we cannot create a new sandbox,\n // that probably means we're on a screensaver worker and the screensaver is down.\n // Try to submit results for completed slices, but return all other non-finished\n // slices to the scheduler -- after a brief delay.\n //\n selectiveDebug() && console.debug('Sandbox terminate handler cannot create new sandbox; evaluator is down', sandbox.identifier);\n this.evaluator.reallyDown = true;\n this.emit('evalDown');\n const delay = 60; // seconds\n this.jobManagerInventory.forEach((jm) => jm.evaluatorDownCleanup(delay));\n this.warning('Stopping all work.', 'Screensaver worker or evaluator may be down.');\n })\n .finally(() => {\n this.sandboxInventory = this.sandboxInventory.filter(sbx => !sbx.isTerminated);\n this.evaluator.shuttingDown = false;\n this.evaluator.downInterlock = false;\n });\n }\n }\n });\n }\n\n /**\n * Terminate extra sandboxes over the limit.\n * @todo XXXpfr Prioritize keeping expensive to assign sandboxes.\n */\n pruneSandboxes ()\n {\n this.sandboxInventory = this.sandboxInventory.filter((sandbox) => !sandbox.isTerminated);\n let pruneCount = this.sandboxInventory.length - this.options.maxSandboxes;\n if (pruneCount <= 0)\n return;\n\n selectiveDebug() && console.debug(`Supervisor.pruneSandboxes START: pruneCount ${pruneCount}/${this.sandboxInventory.length}/${this.options.maxSandboxes}.`, this.dbg.dumpSandboxState());\n selectiveDebug2() && console.debug(this.sandboxInventory.map((sbx) => sbx.identifier));\n\n // Prune ready-for-assign sandboxes first.\n while (pruneCount > 0)\n {\n if (this.sandboxInventory[0].isReadyForAssign)\n {\n const startedSandbox = this.sandboxInventory.shift();\n startedSandbox.terminate(false);\n pruneCount--;\n }\n else\n break;\n }\n\n // Don't purge jobs here: can accidentally purge a job that TD just fetched (XXXpfr)\n\n /**\n * Do we really want to do a bunch of work to keep empty job assigned sandboxes around?\n * When in a private compute group, there will be fewer jobs and it's likely\n * that a given job will be seen again.\n * @todo XXXpfr Prioritize keeping expensive to assign sandboxes.\n */\n const liveJobs = [], emptyJobs = [];\n let maxAssignedSandboxCount = 0;\n for (const jobMan of this.jobManagerInventory)\n {\n let isAlive = false;\n for (const slice of jobMan.sliceInventory)\n if (slice.isQueuedOrActive)\n {\n isAlive = true;\n break;\n }\n if (isAlive)\n liveJobs.push(jobMan);\n else\n {\n const _assignedSandboxes = jobMan.assignedSandboxes;\n if (maxAssignedSandboxCount < _assignedSandboxes.length)\n maxAssignedSandboxCount = _assignedSandboxes.length;\n emptyJobs.push(jobMan);\n }\n }\n\n if (emptyJobs.length > 0)\n {\n // Prune the sandboxes from all jobs with no current work.\n // Try to keep approximately the same # of assigned sandboxes per job.\n for (let k = maxAssignedSandboxCount; k >= 0; k--)\n {\n for (const jobMan of emptyJobs)\n {\n const _assignedSandboxes = jobMan.assignedSandboxes;\n if (_assignedSandboxes.length > k)\n {\n debugging('supervisor') && console.debug(`pruneSandboxes(empty): sandbox${_assignedSandboxes[0].id}`, Date.now() - this.lastTime);\n // Terminate and remove from this.sandboxInventory.\n this.returnSandbox(_assignedSandboxes[0]);\n if (--pruneCount < 1)\n {\n debugging('supervisor') && console.debug(`Supervisor.pruneSandboxes FINISH: ${pruneCount}/${this.sandboxInventory.length}/${this.options.maxSandboxes}.`);\n return;\n }\n }\n }\n }\n }\n\n // Round-robin prune 1 extra assigned sandbox from each non-empty jobmanager.\n while (pruneCount > 0)\n {\n const _pruneCount = pruneCount;\n for (const jobMan of liveJobs)\n {\n const _assignedSandboxes = jobMan.assignedSandboxes;\n if (_assignedSandboxes.length > 0)\n {\n debugging('supervisor') && console.debug(`pruneSandboxes(non-empty): sandbox${_assignedSandboxes[0].id}`, Date.now() - this.lastTime);\n // Terminate and remove from this.sandboxInventory.\n this.returnSandbox(_assignedSandboxes[0]);\n if (--pruneCount < 1)\n {\n debugging('supervisor') && console.debug(`Supervisor.pruneSandboxes FINISH: ${pruneCount}/${this.sandboxInventory.length}/${this.options.maxSandboxes}.`);\n return;\n }\n }\n }\n if (_pruneCount === pruneCount) // Nothing left to prune.\n break;\n }\n\n debugging('supervisor') && console.debug(`Supervisor.pruneSandboxes FINISH: incomplete-prune ${pruneCount}/${this.sandboxInventory.length}/${this.options.maxSandboxes}.`);\n }\n\n // _Idx\n //\n // recordResult, sendToResultSubmitter, sendResultToRemote\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 * @param {Sandbox} sandbox - The sandbox handle associated to the slice.\n * @returns {Promise<any>}\n */\n recordResult (slice, sandbox)\n {\n // It is possible for slice.result to be undefined when there are upstream errors.\n if (!slice.result)\n throw new Error(`Slice ${slice.identifier} completed work, but there is no result. This is ok when there are upstream errors.`);\n if (!slice.isComplete)\n throw new Error(`Cannot record result for slice ${slice.identifier} that has not completed execution successfully.`);\n if (!slice.timeReport)\n throw new Error(`Invalid time report for slice ${slice.identifier} in recordResult`);\n if (!slice.dataReport)\n throw new Error(`Invalid data report for slice ${slice.identifier} in recordResult`);\n\n const metrics = slice.jobManager.updateStatistics(slice, sandbox);\n selectiveDebug() && console.debug(`Supervisor: recording result for slice ${slice.identifier} with metrics`, this.dbg.justCPU(metrics));\n\n /** @see result-submitter::result for full message details */\n const payloadData = {\n slice: slice.sliceNumber,\n job: slice.jobAddress,\n worker: this.workerId,\n paymentAddress: this.options.paymentAddress,\n metrics,\n authorizationMessage: slice.authorizationMessage,\n };\n\n let canceled = false;\n const resultUrl = (slice.resultStorageType !== 'pattern') ? slice.resultStorageDetails : false;\n this.workerEmit( 'beforeResult', () => { canceled = true; }, resultUrl);\n this.jobEmit(slice, 'beforeResult', () => { canceled = true; }, resultUrl);\n selectiveDebug && canceled && console.debug(`User canceled the result submission operation for slice ${slice.identifier}.`);\n if (canceled)\n return this.returnSlice(slice, 'Canceled via beforeResult event');\n\n if (slice.resultStorageType === 'pattern')\n return this.sendResultToRemote(slice)\n .then((response) => {\n return this.sendToResultSubmitter(slice, sandbox.sandboxHandle, payloadData, response);\n });\n\n return this.sendToResultSubmitter(slice, sandbox.sandboxHandle, payloadData, encodeDataURI(slice.result.result));\n }\n\n /**\n * Send result to result submitter.\n * @param {Slice} slice\n * @param {SandboxHandle} sandboxHandle\n * @param {*} payloadData\n * @param {string} [result]\n * @returns {Promise<any>}\n */\n async sendToResultSubmitter (slice, sandboxHandle, payloadData, result)\n {\n // When handleRSError is hit, { slice, payload } is added to the queue this.dcp4.submitResultsQueueMap[slice.key] .\n // For a given slice, the queue is retried independent of other slices that failed to submit.\n // When a given slice hits the retry limit (6 retries) the slice is returned to scheduler.\n const handleRSError = (error, slice, payloadData) => { // eslint-disable-line no-shadow\n const msg = `Failed to submit results to scheduler for slice ${slice.identifier}`;\n if (!error) error = new Error(msg);\n this.error(msg, error);\n\n slice['retrySubmitResults'] = (slice['retrySubmitResults'] ?? 0) + 1;\n if (slice['retrySubmitResults'] > this.options.maxResultSubmissionRetries)\n {\n this.handleFailedSlice(slice, error);\n throw new Error(`Failed to submit results 6 times for slice ${slice.identifier}`);\n }\n\n // For a given slice, there's never more than one element in the corresponding queue.\n this.dcp4.submitResultsQueueMap[slice.key] = [ { slice, sandboxHandle, payloadData } ];\n return this.dcp4.resetConnection('resultSubmitter');\n }\n\n try\n {\n debugging('supervisor') && console.debug('Supervisor.recordResult: payloadData', result.slice(0, 256), slice.identifier);\n if (result)\n payloadData.result = result;\n\n await this.delayManager.nextDelay('recordResult', 2);\n //->console.log('recordResult', slice.identifier, this.evaluator.down, Date.now() - this.lastTime); // SAVE\n\n return this.dcp4.resultSubmitter.request('result', payloadData)\n .then((resp) => {\n const payload = resp.payload;\n if (!resp.success)\n {\n if (payload)\n {\n selectiveDebug() && console.debug('resultSubmitter.send failed', payload);\n throw new DCPError(`Call to result submitter failed when recording results for ${slice.identifier}.`, payload);\n }\n if (debugBuild)\n {\n selectiveDebug() && console.debug('resultSubmitter.send failed with no payload', slice.identifier);\n // Look inside\n for (const [ key, value ] of Object.entries(resp)) {\n if (key !== 'connection')\n console.debug(`${key}:`, value);\n }\n }\n throw new Error(`Call to result submitter failed when recording results for ${slice.identifier}.`);\n }\n\n debugging('supervisor') && console.debug('Successfully submitted results', slice.identifier);\n\n // Success! Restore this['resultSubmitter'] delay to retryMinSleepMs (currently 32ms.)\n // Is there a better way to reset than explicit calls?\n this.delayManager.resetEBO('resultSubmitter');\n\n common.debugQuanta() && this.dbg.addGlobal(slice, payload.metrics);\n slice.jobManager.update({ metrics: payload.metrics }); // Update metrics\n\n // Emit the 3 'payment' events.\n const paymentAddress = payloadData.paymentAddress.toString();\n this.workerEmit( 'payment', payload.slicePaymentAmount, paymentAddress, slice.jobAddress, slice.sliceNumber);\n this.jobEmit(slice, 'payment', payload.slicePaymentAmount, paymentAddress, slice.sliceNumber);\n this.safeEmit(sandboxHandle, 'payment', payload.slicePaymentAmount, paymentAddress);\n\n const payloadLength = kvin.stringify(payloadData).length; /** @TODO - fix per DCP-3750 */\n const resultUrl = (slice.resultStorageType !== 'pattern') ? slice.resultStorageDetails : false;\n this.workerEmit( 'result', resultUrl, payloadLength);\n this.jobEmit(slice, 'result', resultUrl, payloadLength);\n\n slice.markAsFinished();\n\n // Remove the slice from the job manager.\n slice.jobManager.removeSlice(slice);\n\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}.`, Date.now() - this.lastTime);\n }\n if (false)\n {}\n\n return resp;\n })\n .catch ((error) => {\n handleRSError (error, slice, payloadData);\n });\n }\n catch (error)\n {\n handleRSError (error, slice, payloadData);\n }\n }\n\n /**\n * Send a work function's result to a server that speaks our DCP Remote Data Server protocol.\n * E.g. https://gitlab.com/Distributed-Compute-Protocol/dcp-rds\n *\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 */\n sendResultToRemote (slice)\n {\n return supShared.sendResultToRemote(this, slice);\n }\n\n // _Idx\n //\n // handleWorkReject\n //\n\n /**\n * Handles reassigning or returning a slice that rejected.\n *\n * If error.message === 'false' and slice.hasBeenRejected is false, reschedule the slice.\n * Set the slice.hasBeenRejected to be true.\n *\n * If error.message !== 'false' or slice.hasBeenRejected is true (i.e. has been rejected once already)\n * zthen return all slices from the job to the scheduler and terminate all sandboxes with that jobAddress.\n *\n * @param {Slice} slice\n * @param {Error} error\n */\n handleWorkReject (slice, error)\n {\n debugging() && console.debug('handleWorkReject', error.message, slice.hasBeenRejected, slice.identifier);\n\n const jobManager = slice.jobManager;\n jobManager.rejectedJobReasons.push(error.message); // memoize reasons\n\n // First time rejecting without a reason; try rescheduling the slice.\n if (error.message === 'false' && !slice.hasBeenRejected)\n {\n // Mark slice as rejected.\n slice.hasBeenRejected = true;\n // Reset slice state to allow re-execution.\n slice.resetState();\n }\n else\n {\n // Slice has been rejected twice, so add to array of rejected jobs.\n const rejectedJob = {\n address: slice.jobAddress,\n reasons: jobManager.rejectedJobReasons,\n };\n this.rejectedJobs.push(rejectedJob);\n // Broadcast failure.\n this.workerEmit( 'result', error);\n this.jobEmit(slice, 'result', error);\n // Purge the job.\n this.purgeJob(jobManager);\n // Tell everyone all about it, when allowed.\n if (jobManager.displayMaxDiagInfo)\n {\n const suffixMsg = 'All slices and sandboxes with the same jobAddress returned to the scheduler or terminated.';\n if (slice.hasBeenRejected)\n this.warning(`work.reject: The slice ${slice.identifier} was rejected twice.`, suffixMsg);\n else\n this.warning(`work.reject: The slice ${slice.identifier} was rejected with reason: ${error.message}.`, suffixMsg);\n }\n }\n }\n\n}\nexports.Supervisor = Supervisor;\n\n\n//# sourceURL=webpack://dcp/./src/dcp-client/worker/supervisor2/index.js?");
|
|
4727
|
+
eval("/**\n * @file dcp/src/dcp-client/worker/supervisor2/index.js\n * Code managing sandboxes, tasks, jobs, and slices within in a DCP Worker.\n * @author Wes Garland, wes@distributive.network\n * Paul, paul@distributive.network\n * @date Dec 2020\n * June 2022, Jan-April 2023\n * @module supervisor\n * @copyright Copyright (c) 2018-2023, Distributive Corp. All Rights Reserved\n */\n/*\n * initial ready reconnecting stopping stopped paused broken\n * |-- ctor ----------------------------------------------------------------------------------------------------------------->\n * |-- work ----------------------------------------------------------------------------------------------------------------->\n * |-- work --------------------------------------------------------------------------------------------------->\n * |-- work -------------------------------------------------------------------------------->\n * |-- work --------------------------------------------------------->\n * |-- work --------------------------------->\n * |-- Worker.pause --------------------------------------------------------------------------->\n * <-- Worker.unpause -------------------------------------------------------------------------|\n * |-- work ----->\n * |-- stopWork ---------------------------->\n * |-- postStopShutdown --->\n * |-- PM.connectTo --> (ProtocolManager)\n * <-- PM.connectTo --| (ProtocolManager)\n * |-- stopWork ------------------------------------------->\n * <-- work -----------------------------------------------------------------------|\n * <-- stopWork -----------------------------------------------------|\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 DCP_ENV = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\nconst constants = __webpack_require__(/*! dcp/common/scheduler-constants */ \"./src/common/scheduler-constants.js\");\nconst { assert } = __webpack_require__(/*! dcp/common/dcp-assert */ \"./src/common/dcp-assert.js\");\nconst dcp_timers = __webpack_require__(/*! dcp/common/dcp-timers */ \"./src/common/dcp-timers.js\");\nconst EventEmitter = __webpack_require__(/*! events */ \"./node_modules/events/events.js\");\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\nconst { Address } = __webpack_require__(/*! dcp/dcp-client/wallet/eth */ \"./src/dcp-client/wallet/eth.js\");\nconst { Keystore } = __webpack_require__(/*! dcp/dcp-client/wallet/keystore */ \"./src/dcp-client/wallet/keystore.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 { 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 { OriginAccessManager } = __webpack_require__(/*! dcp/dcp-client/worker/origin-access-manager */ \"./src/dcp-client/worker/origin-access-manager.js\");\nconst { a$sleepMs, booley, toJobMap, encodeDataURI, stringify, nextEma } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\nconst kvin = __webpack_require__(/*! kvin */ \"./node_modules/kvin/kvin.js\");\n\nconst { ModuleCache } = __webpack_require__(/*! ./module-cache */ \"./src/dcp-client/worker/supervisor2/module-cache.js\");\nconst { Promise_any } = __webpack_require__(/*! ./promise_any */ \"./src/dcp-client/worker/supervisor2/promise_any.js\");\nconst { ProtocolManager } = __webpack_require__(/*! ./protocol-manager */ \"./src/dcp-client/worker/supervisor2/protocol-manager.js\");\nconst { EvaluatorManager } = __webpack_require__(/*! ./evaluator-manager */ \"./src/dcp-client/worker/supervisor2/evaluator-manager.js\");\nconst { DelayManager } = __webpack_require__(/*! ./delay-manager */ \"./src/dcp-client/worker/supervisor2/delay-manager.js\");\nconst { Options } = __webpack_require__(/*! ./options */ \"./src/dcp-client/worker/supervisor2/options.js\");\nconst common = __webpack_require__(/*! ./common */ \"./src/dcp-client/worker/supervisor2/common.js\");\nconst { debugBuild, selectiveDebug, selectiveDebug2, minimalDiag, selectiveSupEx } = common;\nconst supShared = __webpack_require__(/*! ../SupShared */ \"./src/dcp-client/worker/SupShared.js\");\nconst { canScheduleGPU } = __webpack_require__(/*! ./gpu_support */ \"./src/dcp-client/worker/supervisor2/gpu_support.js\");\n\n/** @typedef {string} opaqueId */ // 22 character base64 string\n/** @typedef {import('./slice2').Slice} Slice */\n/** @typedef {import('dcp/utils/jsdoc-types').Auth} Auth */\n/** @typedef {import('dcp/utils/jsdoc-types').Body} Body */\n/** @typedef {import('./sandbox2').SandboxHandle} SandboxHandle */\n/** @typedef {import('dcp/utils/jsdoc-types').SliceObj} SliceObj */\n/** @typedef {import('dcp/dcp-client/worker/index').Worker} Worker */\n/** @typedef {import('dcp/utils/jsdoc-types').TDPayload} TDPayload */\n/** @typedef {import('dcp/utils/jsdoc-types').Signature} Signature */\n/** @typedef {import('dcp/utils/jsdoc-types').SliceMessage} SliceMessage */\n/** @typedef {import('dcp/dcp-client/wallet/keystore').Keystore} Keystore */\n/** @typedef {import('dcp/utils/jsdoc-types').SupervisorOptions} SupervisorOptions */\n/** @typedef {import('dcp/protocol-v4/connection/connection').Connection} Connection */\n\n//\n// Index to functionality -- search for '_Idx' to toggle through the index.\n//\n// 1) class Supervisor\n// 2) work, checkCapabilities\n// 3) safeEmit, workerEmit, jobEmit, error, warning, mungeError, jobDescriptor, setState\n// 4) returnAllSlices, postStopShutdown, abort, stopWork, purgeJob\n// 5) roundRobinSlices, makeJobSelectionCursor, handleSandboxWorkError, handleFailedSlice\n// 6) returnSlices, returnSlice, emitProgressReport\n// 7) jobQuanta, repoMan, predictLoad(viz., clairvoyance), waitUntilWorkIsReady, generateWorkerComputeGroups\n// 8) availableSandboxSpace, fetchTask, addTaskToWorkload, fetchFromTD, clearUnusedJobManagersAndModuleCache\n// 9) createSandbox, returnSandbox, hookUpSandboxListeners, pruneSandboxes\n// 10) recordResult, sendToResultSubmitter, sendResultToRemote\n// 11) handleWorkReject\n//\n\n// _Idx\n//\n// class Supervisor\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 * Possible states: 'initial', 'ready', 'reconnecting', 'stopping', 'stopped', 'broken'\n * Start state:\n * - initial\n *\n * Intermediate states:\n * - ready\n * - reconnecting\n * - stopping\n *\n * Terminal states:\n * - stopped\n * - broken\n *\n * Valid transitions:\n * - initial -> ready -> reconnecting -> ready\n * - ready -> stopping -> stopped\n * - initial -> broken\n */\nclass Supervisor extends EventEmitter\n{\n /**\n * @constructor\n * @param {Worker} worker\n * @param {Keystore} identity\n * @param {SupervisorOptions} options\n */\n constructor (worker, identity, options)\n {\n super({ captureRejections: false });\n\n if (!(identity instanceof Keystore))\n throw new Error(`identity ${JSON.stringify(identity)} must be an instance of Keystore`);\n\n debugging('supervisor') && console.debug('Supervisor.options', options);\n assert(options === worker.workerOptions);\n \n /** @type {Worker} */\n this.worker = worker;\n /** @type {Keystore} */\n this.identityKeystore = identity;\n /** @type {Options} */\n this.options = new Options(options, worker);\n\n selectiveDebug() && console.debug('Supervisor: cores.cpu, cores.gpu, maxSandboxes', options.cores?.cpu, options.cores?.gpu, this.options.maxSandboxes);\n\n /** @type {ModuleCache} */\n this.moduleCache = new ModuleCache(this);\n\n // Manage delays and exponential backoff.\n this.delayManager = new DelayManager(this, this.options.defaultDelayIncrement);\n\n /* See https://distributive.atlassian.net/browse/DCP-3175 */\n /** @type {OriginAccessManager} */\n this.originManager = OriginAccessManager.construct(this.options.allowOrigins);\n\n /** @type {ProtocolManager} */\n this.dcp4 = new ProtocolManager(this);\n\n /** @type {common.DebuggingTools} */\n this.dbg = new common.DebuggingTools(this);\n\n // Turn on for max speed debugging.\n if (false)\n {}\n\n /** @type { Synchronizer } */\n this.state = new Synchronizer('initial', [ 'initial', 'ready', 'reconnecting', 'stopping', 'stopped', 'paused', 'broken']);\n /** @type {Object<string, JobManager>} */\n this.jobMap = {}; // jobAddress => jobManager\n\n /** @type {JobManager[]} */\n this.jobManagerInventory = common.InventoryArray('jobManagers');\n /** @type {Sandbox[]} */\n this.sandboxInventory = []; // All sandboxes that are being used by the job managers. Makes sure we don't lose sandboxes.\n /** @type {{ next: cbNext, push: cbPush }} */\n this.cursor = null;\n /** @type {number} */\n this.defaultQuanta = 1.0;\n\n /**\n * Evaluator down management.\n **/\n this.evaluator = new EvaluatorManager();\n\n // There are 2 kinds of barriers.\n // 1) fetchTaskBarrier is a barrier for the task fetching from task distributor path.\n // 2) roundRobinBarrier is a barrier for the slice execution path.\n /** @type {boolean} */\n this.fetchTaskBarrier = false;\n /** @type {boolean} */\n this.roundRobinBarrier = false;\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(200); // N = 200 should be more than enough.\n /**\n * When true we await waitUntilWorkIsReady until at least 1 job is ready with at least 1 ready slice.\n * waitUntilWorkIsReady\n * @type {boolean}\n */\n this.waitForWork = true;\n /**\n * Last repoMan time stamp.\n * @type {number}\n **/\n this.lastRepoMan = Date.now();\n /**\n * Last prune time stamp.\n * @type {number}\n **/\n this.lastPrune = Date.now();\n /**\n * General time stamp.\n * @type {number}\n **/\n this.lastTime = Date.now();\n /**\n * Fetch started time stamp.\n * @type {number}\n **/\n this.fetchTaskStarted = 0;\n /**\n * The capabilities of a random sandbox.\n * @todo XXXpfr Re-work this once fetchTask uses the capabilities of every sandbox to fetch slices.\n * @type {object}\n */\n this.capabilities = null;\n /**\n * EMA times series of CPUTime + GPUTime over all jobs.\n * Each EMA entry is computed right before calling fetchTask.\n * @type {number}\n */\n this.localTime = 0;\n /**\n * EMA times series of sliceCPUTime + sliceGPUTime over all jobs.\n * Each EMA entry is computed right before calling fetchTask.\n * @type {number}\n */\n this.globalTime = 0;\n /**\n * When this.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\n try\n {\n // Start up the connections.\n this.dcp4.instantiateAllConnections();\n }\n catch(error)\n {\n this.error('Failed to set up DCP connections:', error);\n this.setState('initial', 'broken');\n this.worker.stop(true).finally(() => { throw error; });\n }\n }\n\n //\n // Compatibility layer between Sup1, Sup2 and the Sup interface exposed by Worker.\n //\n /**\n * Get all sandboxes.\n * @type {Sandbox[]}\n */\n get sandboxes () { return this.sandboxInventory.filter((sandbox) => !sandbox.isTerminated); }\n /**\n * Get all working sandboxes.\n * @type {Sandbox[]}\n */\n get workingSandboxes () { return this.sandboxInventory.filter((sandbox) => sandbox.isWorking); }\n /**\n * Get the number of working sandboxes.\n * @type {number}\n */\n get workingSandboxCount () { return this.workingSandboxes.length; }\n /**\n * Get all slices over all jobs..\n * @type {Slice[]}\n */\n get slices () {\n const slices = [];\n this.jobManagerInventory.forEach((jobManager) => { slices.push(...jobManager.sliceInventory); });\n return slices;\n }\n /**\n * Get all queued slices over all jobs..\n * @type {Slice[]}\n */\n get queuedSlices () {\n const slices = [];\n this.jobManagerInventory.forEach((jobManager) => { slices.push(...jobManager.queuedSlices); });\n return slices;\n }\n /**\n * Get all queued slices over all jobs..\n * @type {Slice[]}\n */\n get workingSlices () {\n const slices = [];\n this.jobManagerInventory.forEach((jobManager) => { slices.push(...jobManager.workingSlices); });\n return slices;\n }\n /** @type {opaqueId} */\n get workerId () { return this.options.workerId; }\n /** @type {opaqueId} */\n set workerId (id) { this.options.workerId = id; }\n get version() { return '2.0.0' }\n /**\n * @deprecated\n * @todo XXXpfr Rip out this sup2/sup1 special-casing when we finally kill sup1.\n * @type {boolean}\n */\n get isSupervisor1 () { return false; }\n /**\n * @deprecated\n * @todo XXXpfr Rip out this sup2/sup1 special-casing when we finally kill sup1.\n * @type {boolean}\n */\n get isSupervisor2 () { return true; }\n\n //\n // Miscellaneous properties.\n //\n\n /**\n * Dynamic maxWorkingCores.\n * The maximum number of cores that can be executing slices. Slices are scheduled\n * using density. E.g. suppose a job has GPUDensity is 0 and CPUDensity is 0.5,\n * then 2 slices of this job can be scheduled on a single core.\n * @type {number}\n */\n get maxWorkingCores () { return this.options.cores?.cpu; }\n /**\n * Dynamic maxWorkingGPUs.\n * The maximum number of GPUs that can be executing slices. Slices are scheduled\n * using density. E.g. suppose a job has GPUDensity is 0.5 and CPUDensity is 0.5,\n * then 2 slices of this job can be scheduled on a single GPU core and a single CPU core.\n * @type {number}\n */\n get maxWorkingGPUs () { return this.options.cores?.gpu; }\n /**\n * @deprecated\n * @todo XXXpfr Get rid of this after Sup1 dies.\n */\n get lastDcpsid () { return this.dcp4.lastDcpsid; }\n /**\n * @deprecated\n * @todo XXXpfr Get rid of this after Sup1 dies.\n */\n set lastDcpsid (dcpsid) { this.dcp4.lastDcpsid = dcpsid; }\n /**\n * Indicates whether supervisor is ready for business.\n * @type {boolean}\n */\n get isReady () { return this.worker.working && this.state.is('ready'); }\n /**\n * The # of sandboxes not being used.\n * @type {number}\n */\n get unusedSandboxCount () { return this.options.maxSandboxes - this.workingSliceCount; }\n /**\n * The unused amount of CPU density in the cores.\n * @type {number}\n */\n get unusedCoreSpace () { return this.maxWorkingCores - this.workingSliceDensity; }\n /**\n * The unused amount of GPU density in the cores.\n * Use Math.max(1, this.maxWorkingGPUs) so there's always enough room to schedule\n * a GPU slice when this.workingGPUDensity = 0. In RoundRobinSlices we use the accumulated\n * recent history ( canScheduleGPU(maxWorkingGPUs) ) to check whether the average recent\n * density is within this.maxWorkingGPUs.\n * @type {number}\n */\n get unusedGPUSpace () { return Math.max(1, this.maxWorkingGPUs) - this.workingGPUDensity; }\n /** @type {number} */\n get workingSliceDensity ()\n {\n let density = 0;\n for (const jobMan of this.jobManagerInventory)\n density += jobMan.workingSliceDensity;\n return density;\n }\n /** @type {number} */\n get workingGPUDensity ()\n {\n let density = 0;\n for (const jobMan of this.jobManagerInventory)\n density += jobMan.workingGPUDensity;\n return density;\n }\n /** @type {number} */\n get workingSliceCount ()\n {\n let count = 0;\n for (const jobMan of this.jobManagerInventory)\n count += jobMan.workingSliceCount;\n return count;\n }\n /**\n * Compute the estimated time to completion of all work.\n * The time is measured as if there were only a single slice running at a time.\n * workRemaining is the amount of time until completion.\n * @type {number}\n */\n get workRemaining ()\n {\n let workRemaining = 0;\n for (const jobMan of this.jobManagerInventory)\n workRemaining += jobMan.workRemaining;\n return workRemaining;\n }\n\n // _Idx\n //\n // work, checkCapabilities\n //\n\n /**\n * Set up sandboxes and interval timers, then start to search for work.\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 */\n work ()\n {\n const abort = async (error) => {\n // May be in a stopping/stopped state, because dcp-worker was hit with ctrl-C.\n this.setState(['ready', 'stopping', 'stopped', 'reconnecting'], 'broken');\n await this.worker.stop(true);\n throw error;\n };\n /* Provide opportunity for calling code to hook ready/error events. */\n dcp_timers.setImmediate(() => {\n try\n {\n // [ 'initial', 'ready', 'reconnecting', 'stopping', 'stopped', 'paused', 'broken' ]\n if (this.state.isNot('initial'))\n {\n if (this.state.in(['ready', 'stopping', 'reconnecting']))\n {\n this.warning(`Supervisor.work was called when supervisor is already ${this.state.valueOf()}.`, 'Please either wait and try again or restart worker.');\n return;\n }\n else if (this.state.is('broken'))\n {\n this.warning(\"Cannot call Supervisor.work when supervisor is in a 'broken' state. Please restart worker.\");\n return;\n }\n this.state.set(['stopped', 'paused'], 'initial');\n }\n this.evaluator.initialize();\n this.dcp4.instantiateAllConnections();\n\n // Beacon interval timer.\n this.progressReportTimer = dcp_timers.setInterval(() => this.emitProgressReport(), this.options.progressReportInterval);\n // Watchdog: fetchTask-driven interval timer.\n this.watchdogTimer = dcp_timers.setInterval(() => this.fetchTask(), this.options.watchdogInterval);\n\n // Interval timers helps keep workers and localExec alive forever.\n this.progressReportTimer.unref();\n this.watchdogTimer.unref();\n\n if ( false || debugging('supervisor'))\n {\n this.sliceDebuggingTimer = setInterval(() => {\n this.jobManagerInventory.forEach((jobMan) => {\n const { unassigned, ready, reserved, working, workdone, complete, failed, finished } = jobMan.dumpSlices ('RRS', false, false);\n console.debug(`RRS(${jobMan.identifier},${this.unusedSandboxCount},${this.unusedCoreSpace},${this.workingSliceCount},${this.workingSliceDensity}): u/r/rsv/w/wd/c/f/fsh ${unassigned}/${ready}/${reserved}/${working}/${workdone}/${complete}/${failed}/${finished}`, jobMan.identifier, this.sandboxInventory.length);\n });\n }, 30 * 1000);\n if (this.sliceDebuggingTimer.unref)\n this.sliceDebuggingTimer.unref();\n }\n\n this.state.set('initial', 'ready');\n\n // Create 1 sandbox now to get the capabilities which are sent to Task Distributor by fetchTask.\n this.createSandbox()\n .then((sandbox) => {\n this.sandboxInventory.push(sandbox);\n debugging('supervisor') && console.debug('work() after createSandbox', this.sandboxInventory.length, sandbox.identifier, Date.now() - this.lastTime, this.options.watchdogInterval);\n this.fetchTask() // Don't wait for watchdog.\n .catch (async (error) => {\n this.error('work() failed when calling fetchTask', error);\n await abort(error);\n });\n })\n .catch(async (error) => {\n this.error('work() failed when calling createSandbox, exiting...', error);\n await abort(error);\n });\n }\n catch(error)\n {\n this.error('work() failed', error);\n if (this.state.is('initial')) this.state.set('initial', 'broken');\n else if (!this.state.is('broken')) this.setState('ready', 'broken');\n this.worker.stop(true).finally(() => { throw error; });\n }\n });\n }\n\n /** Construct capabilities when necessary. */\n checkCapabilities (sandbox)\n {\n /**\n * Assign the capabilities of one the sandboxes before fetching slices from the scheduler.\n * @todo XXXpfr Re-work this once fetchTask uses the capabilities of every sandbox to fetch slices.\n */\n this.capabilities = sandbox.capabilities;\n if (DCP_ENV.isBrowserPlatform && this.capabilities.browser)\n this.capabilities.browser.chrome = DCP_ENV.isBrowserChrome;\n\n debugging('supervisor') && console.debug('Supervisor.checkCapabilities computed', Date.now() - this.lastTime);\n }\n\n // _Idx\n //\n // safeEmit, workerEmit, jobEmit,\n // error, warning, mungeError, jobDescriptor, setState\n //\n\n /**\n * Safe event emitter.\n * @param {EventEmitter} emitter\n * @param {string} event\n * @param {...any} args\n */\n safeEmit(emitter, event, ...args)\n {\n try\n {\n emitter.emit(event, ...args);\n }\n catch (error)\n {\n this.error(`Event handler for event ${event} threw an exception`, error);\n }\n }\n\n /**\n * Safe event emitter on worker.\n * @param {string} event\n * @param {...any} args\n */\n workerEmit(event, ...args)\n {\n this.safeEmit(this.worker, event, ...args);\n }\n\n /**\n * Safe event emitter on slice.jobHandle.\n * @param {Slice} slice\n * @param {string} event\n * @param {...any} args\n */\n jobEmit(slice, event, ...args)\n {\n this.safeEmit(slice.jobHandle, event, ...args);\n }\n\n /**\n * Error feedback to user.\n * @param {string} message\n * @param {Array<Error>|Error|string} [coreError]\n * @param {string} [additionalInfo]\n * @param {boolean} [supressStack=false]\n */\n error (message, coreError, additionalInfo, supressStack = false)\n {\n const isString = (s) => { return (typeof s === 'string' || s instanceof String); };\n if (coreError instanceof AggregateError)\n coreError = coreError.errors;\n if (Array.isArray(coreError) && coreError.length > 0) // Emit error for every element of array.\n return coreError.flat().forEach((c_err) => this.error(message, c_err, additionalInfo));\n\n debugging('supervisor') && console.debug('Supervisor.error:', message, coreError, additionalInfo);\n if (!message)\n message = 'Supervisor.error called w/o valid message';\n if (additionalInfo)\n {\n if (typeof additionalInfo === 'object')\n // @ts-ignore\n additionalInfo = (additionalInfo instanceof Error) ? additionalInfo.message : JSON.stringify(additionalInfo);\n else if (typeof additionalInfo !== 'string')\n additionalInfo = String(additionalInfo);\n\n if (!isString(additionalInfo))\n additionalInfo = additionalInfo.toString();\n if (!coreError)\n coreError = '';\n else if (!isString(coreError))\n coreError = String(coreError);\n }\n\n let dcpError;\n if (additionalInfo)\n dcpError = new DCPError(message, coreError, additionalInfo, supressStack);\n else if (coreError && (coreError instanceof Error))\n dcpError = new DCPError(message, coreError, '', supressStack);\n else\n dcpError = new DCPError(message, '', '', supressStack);\n\n this.worker.emit('error', dcpError);\n }\n\n /**\n * Warning feedback to user.\n * @param {string[]} messages\n */\n warning (...messages)\n {\n debugging('supervisor') && console.debug('Supervisor.warning:', messages);\n if (messages.length < 1)\n messages = [ 'Supervisor.warning called w/o valid message(s)' ];\n messages.forEach((message) => this.worker.emit('warning', message));\n }\n\n /**\n * @deprecated\n * Create new object and copy the interesting properties from error.\n * Only show the stack for debug builds.\n * If timestamp isn't set, assign new Date().\n * @param {{ message }|string|object} error\n * @param {*} [errorCtor]\n * @returns {string|{ message }}\n */\n __mungeError (error, errorCtor)\n {\n if (typeof error === 'string')\n {\n const errorLines = error.split('\\n');\n return common.displayMaxDiagInfo() ? error : errorLines[0];\n }\n\n if (!error || typeof error !== 'object' || !('message' in error) || Array.isArray(error))\n return error;\n\n if (minimalDiag)\n return error.message;\n\n const errorObj = errorCtor ? new errorCtor(error.message) : { message: error.message };\n\n const props = common.displayMaxDiagInfo()\n ? [ 'type', 'process', 'name', 'origin', 'info', 'code', 'errorCode', 'operation', 'fileName', 'lineNumber', 'timestamp' ]\n : [ 'code', 'errorCode', 'fileName', 'lineNumber', 'timestamp' ]\n const predCopy = (prop) => {\n if (error[prop])\n errorObj[prop] = error[prop];\n };\n\n props.forEach((prop) => { predCopy(prop); });\n\n if (common.displayMaxDiagInfo())\n {\n predCopy('stack');\n if (errorObj['name'] === 'Error')\n delete errorObj['name'];\n }\n if (!errorObj['timestamp'])\n errorObj['timestamp'] = new Date();\n\n return errorObj;\n }\n\n /**\n * Get the job descriptor for the appropriate job manager,\n * which is the object value corresponding to jobAddress, in\n * the object returned by getJobsForTask in task-jobs.js.\n * @param {string} jobAddress\n * @returns {object}\n */\n jobDescriptor (jobAddress)\n {\n const jobManager = this.jobMap[jobAddress];\n if (!jobManager)\n throw new Error(`Cannot find the job descriptor corresponding to jobAddress ${jobAddress}`);\n return jobManager.jobMessage;\n }\n\n /**\n * Protect this.state when transitioning from currState -> nextState\n * It's dangerous to place this.state.set in a catch block with this.error or this.warning\n * because an uncaught exception will kill process before emitting the event-based diagnostic.\n * @param {string|string[]} currState\n * @param {string} nextState\n */\n setState(currState, nextState)\n {\n try { this.state.set(currState, nextState); }\n catch (e) { this.error('Supervisor.state.set error', e); }\n }\n\n // _Idx\n //\n // returnAllSlices, postStopShutdown, abort\n // stopWork, purgeJob\n //\n\n /** @returns {Promise<*>} */\n returnAllSlices ()\n {\n if (selectiveDebug())\n {\n const activeSlices = this.jobManagerInventory.map((jm) => jm.activeSlices).flat();\n if (activeSlices.length > 0)\n this.warning(`Returning active slices : ${stringify(activeSlices.map((slice) => slice.identifier), -1, 2)}`);\n }\n // The promises are all about returning the slices to the scheduler and there's no reason to await that.\n return Promise.all(this.jobManagerInventory.map((jm) => jm.destroy()));\n }\n\n /** @returns {Promise<*>} */\n postStopShutdown ()\n {\n for (const sandbox of this.sandboxInventory)\n sandbox.terminate(false);\n this.sandboxInventory = [];\n\n // There shouldn't be anything in the job managers, but just to be safe call returnAllSlices.\n // Clear jobManagerInventory, close all connections and set state to 'stopped'.\n return this.returnAllSlices()\n .finally(() => {\n // Re-enable is-screen-saver-active logic for the sandbox handle 'end' event handler.\n this.evaluator.pauseSandboxHandleEndHandler = false;\n this.jobManagerInventory = common.InventoryArray('jobManagers');\n return this.dcp4.closeConnections()\n .finally (() => {\n if (this.state.isNot('stopped'))\n this.setState('stopping', 'stopped');\n // This log message assume slices were returned to scheduler in a previous operation, which is the only current use case.\n // If we use this function in a different way in the future, update the log message.\n selectiveDebug() && console.debug(`Supervisor.postStopShutdown(${this.state}): terminated all sandboxes and returned all slices to scheduler...`);\n });\n });\n }\n\n /**\n * Stop the worker immediately and return all unfinished slices.\n * @returns {Promise<*>}\n */\n abort ()\n {\n return this.returnAllSlices()\n .finally (() => {\n return this.postStopShutdown();\n });\n }\n\n /**\n * Terminates sandboxes and returns slices.\n * Sets the working flag to false, call @this.work to start working again.\n *\n * If forceTerminate is true: Terminates all sandboxes and returns all slices.\n * If forceTerminate is false: Terminates non-working sandboxes and returns 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<*>}\n */\n async stopWork (forceTerminate = true)\n {\n /** @returns {boolean} */\n const doNotWaitForWork = () => {\n return (this.evaluator.reallyDown || !this.sandboxInventory.filter(sbx => !sbx.isTerminated).length);\n }\n selectiveDebug() && console.debug(`Supervisor.stopWork(${forceTerminate}, ${this.state}): terminating sandboxes and returning slices to scheduler.`);\n\n // [ 'initial', 'ready', 'reconnecting', 'stopping', 'stopped', 'paused', 'broken']\n if (this.state.in(['stopping', 'stopped', 'reconnecting']))\n {\n this.warning(`Supervisor.stopWork was called when supervisor is in state ${this.state.valueOf()}.`, 'Please either wait and try again or restart worker.');\n return;\n }\n else if (this.state.is('initial'))\n {\n this.warning('Cannot call stopWork before worker has started. Please either wait and try again or restart worker.');\n return;\n }\n this.state.set(['ready', 'paused', 'broken'], 'stopping');\n\n this.dcp4.instantiateAllConnections();\n\n // Do not enter is-screen-saver-active logic in the sandbox handle 'end' event handler.\n this.evaluator.pauseSandboxHandleEndHandler = true;\n\n if (forceTerminate)\n return this.abort();\n else\n {\n const slicesToReturn = [];\n for (const jm of this.jobManagerInventory)\n slicesToReturn.push(...jm.queuedSlices);\n\n const reason = `stopWork returning all non-finished slices that are not working`;\n this.returnSlices(slicesToReturn, reason);\n\n for (let k = 0; k < 3; k++)\n {\n await new Promise((resolve) => {\n // Count the slices that have been working or close-to-working but haven't submitted results yet.\n let activeSliceCount = 0;\n for (const jm of this.jobManagerInventory)\n activeSliceCount += jm.activeSlices.length;\n // When no active slices we're done.\n if (activeSliceCount === 0)\n resolve();\n // When no work can be completed we return all slices and leave.\n if (doNotWaitForWork())\n {\n this.returnAllSlices();\n resolve();\n }\n selectiveDebug() && console.debug(`StopWork: waiting for ${activeSliceCount} working slices to finish`, k);\n // Resolve and finish stopWork once all sandboxes have finished submitting their results.\n this.worker.on('result', () => {\n selectiveDebug() && console.debug(`StopWork: result handler, activeSliceCount ${activeSliceCount-1}`);\n if (--activeSliceCount === 0)\n {\n this.warning('All sandboxes empty, stopping worker and closing all connections');\n resolve();\n }\n });\n this.on('evalDown', () => {\n this.warning('Evaluator is down.', 'Force return all slices to scheduler, stopping worker and closing all connections.');\n this.returnAllSlices();\n resolve();\n });\n });\n }\n\n for (const jm of this.jobManagerInventory)\n this.safeEmit(jm.jobHandle, 'flush');\n\n if (selectiveDebug())\n {\n console.debug(`stopWork(${this.state.valueOf()}): After waiting for working slices to finish: workingSbxes: ${this.workingSandboxCount}, totalSbxes: ${this.sandboxInventory.length}, jobs: ${this.jobManagerInventory.length}`);\n this.jobManagerInventory.forEach((jm) => {\n console.debug('stopWork job', jm.identifier);\n console.debug(jm.countSliceStr('stopWork'));\n });\n }\n }\n\n return this.postStopShutdown();\n }\n\n /**\n * Purge all traces of the job.\n * @param {JobManager} jobManager\n */\n purgeJob (jobManager)\n {\n selectiveDebug() && console.debug(`Supervisor.purgeJob ${jobManager.identifier}.`);\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 !== jobManager.address);\n this.jobManagerInventory.delete(jobManager);\n this.moduleCache.removeJob(jobManager.address);\n this.dbg.cleanUpDeadJob(jobManager.address);\n jobManager.destroy();\n }\n\n // _Idx\n //\n // roundRobinSlices, makeJobSelectionCursor, handleSandboxWorkError, handleFailedSlice\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 *\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 based on the following ruleset:\n * 1) cursor = makeJobSelectionCursor(), then cursor.next() returns a slice chosen as follows.\n * 2) Let concurrency range from 1 to maxWorkingCores.\n * 3) For a given concurrency, let readyJobs be all jobs such that jobMan.workingSliceDensity < concurrency.\n * 4) Do an ascending sort of readyJobs wrt jobMan.emaTotalTime.\n * 5) Pick a slice from the longest job in readyJobs that doesn't have any executing slices.\n * 6) Alternately shift a slice from readySlices vs choose a slice from a random nearly finished job, and remove slice from readySlices.\n * 7) When there are no more almost finished jobs with slices, shift slices from readyJobs.\n * 8) Jobs which have slicePriority set by the task-distributor may have slices chosen ahead of the above algorithm.\n * 9) Jobs with a slicePriority closer to 1 are more likely to be chosen.\n * 10) After finishing concurrency at maxWorkingCores, cursor.next() returns null, so create a new cursor.\n * @returns {Promise<any>}\n */\n roundRobinSlices ()\n {\n //\n // Should we try to put all runSlice promises in an array and return Promise.all(runslice-promises) ?\n //\n try\n {\n /**\n * The amount of space available for the CPU-component of slices to run in sandboxes.\n * If space is 2.5 and there are 6 slices with density 0.4, and there are enough non-working usable\n * sandboxes, then all 6 slices will be scheduled to run.\n * @type {number}\n */\n const unusedCoreSpace = this.unusedCoreSpace;\n /**\n * The number of sandboxes not currently being used.\n * @type {number}\n */\n const unusedSandboxCount = this.unusedSandboxCount;\n /**\n * The amount of space available for the GPU-component of slices to run in sandboxes.\n * @type {number}\n */\n const unusedGPUSpace = this.unusedGPUSpace;\n if (unusedCoreSpace < common.doNotSchedule || this.roundRobinBarrier || unusedSandboxCount < 1)\n {\n selectiveDebug2() && console.debug('RRS: bail early space/barrier/unusedSlots', unusedCoreSpace, this.roundRobinBarrier, unusedSandboxCount);\n return;\n }\n // roundRobinBarrier is a barrier for the slice execution path.\n this.roundRobinBarrier = true;\n if (this.evaluator.down && this.evaluator.createSandboxRefCount > 0)\n return;\n selectiveDebug2() && console.debug('BarrierState:RRS:', this.fetchTaskBarrier, this.roundRobinBarrier);\n\n if (selectiveDebug2())\n {\n this.jobManagerInventory.forEach((jobMan) => {\n const { unassigned, ready, reserved, working, workdone, complete, failed, finished } = jobMan.dumpSlices ('RRS', false, false);\n console.debug(`RRS(${jobMan.identifier},${unusedSandboxCount},${unusedCoreSpace}): u/r/rsv/w/wd/c/f/fsh ${unassigned}/${ready}/${reserved}/${working}/${workdone}/${complete}/${failed}/${finished}`, jobMan.identifier, this.sandboxInventory.length);\n });\n }\n\n if ( false || selectiveDebug())\n {\n let totalReady = 0, totalReadyDensity = 0;\n for (const jobMan of this.jobManagerInventory)\n {\n const currentReady = jobMan.readySlices.length;\n const currentReadyDensity = jobMan.readySlices.length * jobMan.estimateDensity;\n totalReady += currentReady;\n totalReadyDensity += currentReadyDensity;\n console.debug(`RRS: job ${jobMan.identifier}, density ${jobMan.estimateDensity}, readySlices ${currentReady}, readyDensity ${currentReadyDensity}`);\n }\n console.debug(`RRS: space ${unusedCoreSpace}, unusedSandboxCount ${unusedSandboxCount}, totalReady ${totalReady}, totalReadyDensity ${totalReadyDensity}`);\n }\n\n /** @type {Slice[]} */\n const slices = [];\n /** @type {number} */\n let density = 0;\n /** @type {number} */\n let gpuDensity = 0;\n\n const isSpaceAvailable = (density) => {\n const result = density < unusedCoreSpace && slices.length < unusedSandboxCount;\n selectiveDebug2() && console.debug('RRS: isSpaceAvailable', density < unusedCoreSpace, slices.length < unusedSandboxCount);\n return result;\n }\n\n // When the cursor is almost done and RRS tries to schedule slices,\n // it makes sense to recreate the cursor once to ensure enough slices can be pulled from cursor.\n let recreateCursorCount = 0;\n\n while (isSpaceAvailable(density + common.schedulingSlop))\n {\n // Get existing cursor or create new one.\n if (!this.cursor)\n this.cursor = this.makeJobSelectionCursor();\n\n // Get the next slice, then check to see whether it can be used.\n const slice = this.cursor.next();\n if (!slice)\n {\n if (/*!okToSchedule ||*/ ++recreateCursorCount > 1)\n {\n this.cursor = null;\n break;\n }\n // Start a new cursor.\n this.cursor = this.makeJobSelectionCursor();\n continue;\n }\n let okToSchedule = true\n const job = slice.jobManager;\n density += job.estimateDensity;\n if (job.useGPU)\n {\n okToSchedule = canScheduleGPU(this.maxWorkingGPUs);\n if (okToSchedule)\n {\n gpuDensity += job.estimateGPUDensity;\n okToSchedule = (gpuDensity <= unusedGPUSpace);\n selectiveDebug2() && console.debug(`RRS: GPU scheduling(${okToSchedule},${this.workingSliceCount},${density.toFixed(7)},${unusedCoreSpace.toFixed(7)}): gpuDensity/gpuSpace ${gpuDensity.toFixed(7)}/${unusedGPUSpace.toFixed(7)}, jobGPUDensity/jobCPUDensity ${job.estimateGPUDensity.toFixed(7)}/${job.estimateDensity.toFixed(7)}`);\n }\n }\n if (okToSchedule && density <= unusedCoreSpace + common.schedulingSlop) // Ok, if it's only over by a little bit.\n slices.push(slice);\n else\n {\n slice.unReserve();\n density -= job.estimateDensity;\n if (job.useGPU)\n gpuDensity -= job.estimateGPUDensity;\n else\n this.cursor.push(slice); // If useGPU, then skip pulling a slice from job\n break;\n }\n selectiveDebug2() && console.debug('RRS: density/space/numSlices/unusedSlots/jobDensity', density, unusedCoreSpace, slices.length, unusedSandboxCount, job.estimateDensity);\n }\n\n selectiveSupEx() && density > 0 && console.debug(`roundRobinSlices(${this.workingSliceCount},${this.workingSliceDensity}): Found density ${density.toFixed(7)}/${unusedCoreSpace} with ${slices.length} slices:`, slices.map((slice) => slice.identifier), this.jobManagerInventory.map((jm) => `${jm.identifier}:${jm.estimateDensity.toFixed(7)}:${jm.emaSliceTime.toFixed(0)}`));\n\n // Execute the slices.\n if (slices.length > 0)\n {\n const lastSlice = slices.pop();\n for (const slice of slices)\n slice.jobManager.runSlice(slice);\n return lastSlice.jobManager.runSlice(lastSlice);\n }\n }\n finally\n {\n this.roundRobinBarrier = false;\n }\n }\n\n /**\n * @private\n * @callback cbNext\n * @returns {Slice}\n */\n /**\n * @private\n * @callback cbPush\n * @param {Slice} 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 based on the following ruleset:\n * 1) cursor = makeJobSelectionCursor(), then cursor.next() returns a slice chosen as follows.\n * 2) Let concurrency range from 1 to maxWorkingCores.\n * 3) For a given concurrency, let readyJobs be all jobs such that jobMan.workingSliceDensity < concurrency.\n * 4) Do an ascending sort of readyJobs wrt jobMan.emaTotalTime.\n * 5) Pick a slice from the longest job in readyJobs that doesn't have any executing slices.\n * 6) Alternately shift a slice from readySlices vs choose a slice from a random nearly finished job, and remove slice from readySlices.\n * 7) When there are no more almost finished jobs with slices, shift slices from readyJobs.\n * 8) Jobs which have slicePriority set by the task-distributor may have slices chosen ahead of the above algorithm.\n * 9) Jobs with a slicePriority closer to 1 are more likely to be chosen.\n * 10) After finishing concurrency at maxWorkingCores, cursor.next() returns null, so create a new cursor.\n *\n * A custom selection of jobs can be passed in via the argument jobManagers.\n *\n * @param {JobManager[]} [jobManagers]\n * @returns {{ next: cbNext, push: cbPush }}\n */\n makeJobSelectionCursor (jobManagers)\n {\n /* Variables in this scope function as state information for next() */\n /** @type {JobManager[]} */\n var candidateJobs; // The jobs available with slices ready to execute.\n /** @type {JobManager[]} */\n var readyJobs; // The jobs from which slices are selected for a given concurrency level.\n /** @type {JobManager[]} */\n var preferedJobs = []; // Those jobs in readyJobs with a slicePreference property.\n /** @type {JobManager[]} */\n var lowDensityJobs = []; // Jobs with density <= 0.6, will be scheduled again.\n /** @type {Slice[]} */\n var pendingSlices = [];\n /**\n * Upper bound of the sum of the working slices densities allowed for a given job.\n * type {number}\n **/\n var concurrency = 0;\n /** type {number} */\n var jobIdx = 0;\n /** @type {boolean} */\n var lowDensityPass = false;\n\n const that = this;\n if (!jobManagers)\n jobManagers = this.jobManagerInventory;\n\n const jobStateStr = (jobs) => {\n return jobs.map((jm) => `${jm.identifier} : ${jm.readySlices.length} : ${jm.workingSliceDensity} : ${Math.round(jm.emaTotalTime)}`);\n }\n const jobState = (hdr, jobs) => { console.debug(hdr, jobStateStr(jobs)); }\n\n /**\n * Populate readyJobs with jobs which are ready and have at least one slice which is ready,\n * and whose # of working slice density is less than concurrency. A reserved slice has a\n * finite lifetime and if exceeded, transition it back to ready.\n * @param {JobManager[]} jobManagers\n * @param {number} concurrency\n */\n function filterJobsAndCheckOldReservedSlices (jobManagers, concurrency) // eslint-disable-line no-shadow\n {\n candidateJobs = [], readyJobs = [];\n const fiveMinutesAgo = Date.now() - that.options.reservedSliceLifetime;\n for (const jobMan of jobManagers)\n {\n if (!jobMan.ready) continue;\n let readyCount = 0;\n for (const slice of jobMan.sliceInventory)\n {\n if (slice.isReady) readyCount++;\n else if (slice.isReserved && fiveMinutesAgo > slice.startTime)\n {\n slice.unReserve();\n readyCount++;\n }\n }\n if (readyCount > 0)\n {\n candidateJobs.push(jobMan);\n if (jobMan.workingSliceDensity < concurrency) readyJobs.push(jobMan);\n }\n }\n }\n\n function seed (concurrency) // eslint-disable-line no-shadow\n {\n /* Reset. */\n jobIdx = 0;\n\n /* Populate readyJobs with jobs which are ready and have at least one slice which is ready,\n and whose # of working slice density is less than concurrency. */\n filterJobsAndCheckOldReservedSlices(jobManagers, concurrency);\n // candidateJobs = jobManagers.filter((jobMan) => jobMan.readySlices.length > 0);\n // readyJobs = candidateJobs.filter((jobMan) => jobMan.workingSliceDensity < concurrency);\n\n if (!lowDensityPass && lowDensityJobs.length === 0)\n lowDensityJobs = jobManagers.filter((jm) => jm.estimateDensity > 0 && jm.estimateDensity <= 0.6);\n\n if (readyJobs.length > 1)\n {\n /* Asc sort by shortest average slice completion time. */\n const shortestSliceJobs = readyJobs.sort((a, b) => Math.round(a.emaTotalTime) - Math.round(b.emaTotalTime));\n const almostDoneIndices = shortestSliceJobs.filter((jm) => jm.almostDone).map((_, idx) => idx);\n readyJobs = [];\n\n /* Find longest job that isn't working. */\n for (let k = shortestSliceJobs.length - 1; k >= 0; k--)\n {\n const jobMan = shortestSliceJobs[k];\n if (jobMan.isNotWorking)\n {\n readyJobs.push(jobMan);\n shortestSliceJobs.splice(k, 1);\n break;\n }\n }\n\n /* Alternate the next shortest slice with a random almost done job. */\n if (almostDoneIndices.length > 0)\n {\n while (shortestSliceJobs.length > 0)\n {\n readyJobs.push(shortestSliceJobs.shift());\n if (almostDoneIndices.length < 1)\n break;\n else\n {\n const almostDoneIdx = almostDoneIndices[Math.floor(Math.random() * almostDoneIndices.length)];\n readyJobs.push(shortestSliceJobs[almostDoneIdx]);\n shortestSliceJobs.splice(almostDoneIdx, 1);\n }\n }\n }\n if (shortestSliceJobs.length > 0)\n readyJobs.push(...shortestSliceJobs);\n }\n /* Populate preferedJobs with jobs from readyJobs which also have a slicePreference set. */\n preferedJobs = candidateJobs.filter((jm) => jm.hasOwnProperty('slicePreference'));\n selectiveDebug2() && jobState(`makeJobSelectionCursor:seed(${concurrency}): readyJobs:`, readyJobs);\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 if (pendingSlices.length > 0)\n {\n const slice = pendingSlices.pop();\n return slice.markAsReserved();\n }\n\n if (concurrency === 0)\n seed(++concurrency);\n\n selectiveDebug2() && console.debug(`makeJobSelectionCursor(cc/idx/ready/working):next(${concurrency},${jobIdx},${readyJobs.length},${that.workingSliceDensity}): maxWorkingCores ${that.maxWorkingCores}: begin`);\n while (true)\n {\n if (jobIdx >= readyJobs.length)\n {\n if (++concurrency > that.maxWorkingCores)\n break;\n seed(concurrency);\n }\n\n if (readyJobs.length < 1)\n {\n if (candidateJobs.length < 1)\n break;\n continue; /* No ready jobs at current concurrency level. */\n }\n\n selectiveDebug2() && console.debug(`makeJobSelectionCursor:next(${concurrency},${jobIdx},${readyJobs.length},${that.workingSliceDensity}): before loop`);\n\n /* Schedule a prefered job slice based on random chance. */\n if (preferedJobs.length > 0)\n {\n let prioRan = Math.random();\n let list = preferedJobs.filter((jm) => jm['slicePreference'] >= prioRan);\n\n if (list.length > 0)\n {\n const jobMan = list[list.length * Math.random()];\n const slice = jobMan.reserveOneSlice();\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 (jobIdx < readyJobs.length)\n {\n const jobMan = readyJobs[jobIdx];\n const slice = jobMan.reserveOneSlice();\n if ( false || selectiveDebug2())\n {\n slice && console.debug(`makeJobSelectionCursor:next(${concurrency},${lowDensityPass},${jobIdx},${readyJobs.length},${that.workingSliceDensity}): found slice(slice:ready:working)`, `${slice.identifier} : ${slice.jobManager.readySlices.length} : ${slice.jobManager.workingSliceDensity}`);\n !slice && console.debug(`makeJobSelectionCursor:next(${concurrency},${lowDensityPass},${jobIdx},${readyJobs.length},${that.workingSliceDensity}): no slices ready for job`, jobMan.identifier);\n }\n jobIdx++;\n if (slice)\n return slice;\n }\n\n /*\n * We did not schedule a slice with current seed. We need to re-seed to look for newly-available work\n * and sandboxes, ratcheting up the concurrency (max # of each job running) until we find something.\n */\n selectiveDebug2() && console.debug(`makeJobSelectionCursor:next(${concurrency},${jobIdx},${readyJobs.length},${that.workingSliceDensity}): after loop`);\n }\n if (!lowDensityPass && lowDensityJobs.length > 0)\n {\n jobManagers = lowDensityJobs;\n concurrency = 0;\n lowDensityPass = true;\n return next();\n }\n selectiveDebug2() && console.debug(`makeJobSelectionCursor:next(${concurrency},${jobIdx},${readyJobs.length},${that.workingSliceDensity}): null`, lowDensityPass);\n return null; /* Did not find any more work that fits. */\n }\n function push (slice) { pendingSlices.push(slice); }\n\n return { next, push };\n }\n\n\n /**\n * Handle sandbox.work(...) errors.\n * @todo The orginal code from 2019 did not terminate sandbox when not SandboxError and Sandbox code didn't already terminate. Do we want to try that?\n * The old 2019 sandbox code terminated upon error in start, assign, resetState, describe, applyRequirements and work.\n * So maybe that 2019 terminate yoga was a bunch of hooie.\n * @param {Sandbox} sandbox\n * @param {Slice} slice\n * @param {Error} error\n * @returns {string}\n */\n handleSandboxWorkError (sandbox, slice, error)\n {\n if (debugBuild && !(slice.isWorking || slice.isWorkDone)) // Sanity. Exception should never fire.\n throw new Error(`handleSandboxWorkError: slice ${slice.identifier} must be WORKING.`);\n\n /** @type {boolean} */\n const isSandboxError = error instanceof SandboxError;\n /** @type {string} */\n let reason;\n const jobAddress = common.truncateAddress(slice.jobAddress);\n\n if (isSandboxError)\n reason = error['errorCode']\n else\n {\n // This error was unrelated to the work being done.\n reason = 'Slice has failed to complete execution';\n if (!error)\n error = new Error(`Slice ${slice.sliceNumber} in state ${slice.state} of job ${jobAddress} failed to complete execution`);\n }\n selectiveDebug() && console.debug('handleSandboxWorkError', slice.identifier, error);\n\n let errorString, onlyDisplayErrorString = true;\n if (error.name === 'EWORKREJECT')\n {\n reason = 'EWORKREJECT'; // The status.js processing does not have a case for 'EWORKREJECT'.\n errorString = !slice.hasBeenRejected\n ? `Slice rejected work: ${error.message}.`\n : `Slice rejected work twice; terminate job: ${error.message}.`\n error.stack = 'Sandbox was terminated by work.reject()';\n this.handleWorkReject(slice, error);\n }\n else\n {\n if (!this.evaluator.down)\n {\n /** Do we to be more selective when we retry a slice? */\n if (/*!isSandboxError ||*/ slice['useRetryLogic'])\n {\n slice['sandboxErrorCount'] = ( slice['sandboxErrorCount'] ?? 0) + 1;\n if (slice['sandboxErrorCount'] <= this.options.maxSliceRetries)\n slice.resetState(); // Try to reuse the slice.\n }\n }\n if (!slice.isReady)\n {\n selectiveDebug() && console.debug(`handleSandboxWorkError: returning slice ${slice.identifier}`);\n this.returnSlice(slice, reason)\n .finally (() => {\n this.handleFailedSlice(slice, error)\n });\n }\n\n switch (reason)\n {\n case 'ENOPROGRESS':\n errorString = 'No progress error in sandbox.';\n break;\n case 'ESLICETOOSLOW':\n errorString = 'Slice too slow error in sandbox.';\n break;\n case 'EPERM_ORIGIN':\n errorString = `Could not fetch data; origin not allowed: ${error.message}.`;\n break;\n case 'EFETCH':\n errorString = `Could not fetch data: ${error.message}.`;\n break;\n case 'EUNCAUGHT':\n onlyDisplayErrorString = false;\n errorString = `Uncaught error in sandbox: ${error.message}.`;\n break;\n default:\n onlyDisplayErrorString = false;\n errorString = `Slice failed in sandbox: ${error.message}.`;\n break;\n }\n }\n\n // Always terminate sandbox.\n this.returnSandbox(sandbox);\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,\n sliceNumber: slice.sliceNumber,\n sandbox: sandbox.id,\n jobName: sandbox.public ? sandbox.public.name : 'unnamed',\n };\n\n if (!displayMaxInfo && onlyDisplayErrorString)\n this.error(errorString, '', '', true);\n else\n {\n Object.entries(errorObject).forEach(([k,v]) => (errorString += `\\n ${k}: ${v}`));\n this.error(errorString, error, '', true);\n }\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 */\n handleFailedSlice (slice, error)\n {\n debugging('supervisor') && console.debug(`handleFailedSlice: ${slice.identifier}`, error);\n slice.collectResult(error, false /*success*/);\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.workerEmit( 'result', error);\n this.jobEmit(slice, 'result', error);\n }\n\n // _Idx\n //\n // returnSlices, returnSlice, emitProgressReport\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 slice candidates to check if they can be returned to the scheduler.\n * @param {string} reason - Reason for the return: 'ENOPROGRESS', 'EUNCAUGHT', 'ESLICETOOSLOW', 'EPERM_ORIGIN', 'EFETCH', 'unknown'.\n * @returns {Promise<*>} - Response from the scheduler.\n */\n returnSlices (slices, reason)\n {\n /** @param {Slice[]} verifiedSlices */\n const compressPayload = (verifiedSlices) => {\n assert(verifiedSlices?.length > 0);\n if (verifiedSlices.length > 1)\n return {\n worker: this.workerId,\n slices: common.constructReturnSliceBuckets(verifiedSlices, reason),\n };\n return verifiedSlices[0].getReturnMessagePayload(this.workerId, reason);\n }\n\n if (!slices || !slices.length)\n return Promise.resolve();\n\n debugging('supervisor') && console.debug(`Supervisor.returnSlices(${this.state}): Returning slices`, slices.map(slice => slice.identifier));\n\n // Only return those slices which still exist in their respective jobManagers sliceInventory .\n const verifiedSlices = slices.filter((slice) => slice.jobManager.removeSlice(slice));\n if (verifiedSlices.length > 0)\n {\n selectiveSupEx() && console.debug('Supervisor.returnSlices: Returning slices', verifiedSlices.map(slice => slice.identifier));\n return this.dcp4.sliceReturn(compressPayload(verifiedSlices), slices, reason);\n }\n return Promise.resolve();\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(...) .\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 - Reason for the return: ''ENOPROGRESS', 'EUNCAUGHT', 'ESLICETOOSLOW', 'EPERM_ORIGIN', 'EFETCH', 'unknown'.\n * @returns {Promise<*>} - Response from the scheduler.\n */\n returnSlice (slice, reason) { return this.returnSlices([ slice ], reason); }\n\n /**\n * Send beacon to status.js for 'progress' and sliceStatus.scheduled.\n *\n * Run in an interval created in the ctor.\n * @returns {void|Promise<*>}\n */\n emitProgressReport ()\n {\n const readySlices = [], workingSlices = [];\n this.jobManagerInventory.forEach((jobManager) => {\n readySlices.push(...jobManager.readySlices);\n workingSlices.push(...jobManager.workingSlices);\n });\n /** @type {SliceObj[]} */\n const slices = common.constructSliceBuckets( readySlices, sliceStatus.scheduled );\n common.constructSliceBuckets( workingSlices, 'progress', slices );\n\n debugging('supervisor') && console.debug('emitProgressReport:', stringify(slices));\n\n if (slices.length > 0)\n {\n const payload = { worker: this.workerId, slices };\n return this.dcp4.safeRSStatus(payload, 'Failed to emit progress report');\n }\n }\n\n // _Idx\n //\n // jobQuanta, repoMan, predictLoad(viz., clairvoyance), waitUntilWorkIsReady, generateWorkerComputeGroups\n //\n\n /**\n * For a given job, the scheduler stores an EMA approximation of slice completion time.\n * However, each worker also tracks the same information and the ratio of local-info to\n * scheduler-info (viz., global-info) is returned by this.jobQuanta so we can tell the\n * task distributor how much work to return from fetchTask so that the work actually takes\n * 5 minutes to complete when using all the worker sandboxes.\n * @returns {Object<string, number>}\n */\n jobQuanta ()\n {\n //\n // Prevent wild swings of this.defaultQuanta, which is roughly the ratio of\n // local_worker_slice_time / jobPerfData_measured_slice_time.\n // We limit this ratio to be between 1/8th and 8.\n const minQuanta = 0.125, maxQuanta = 8.0;\n //\n // Because there will be slgiht differences between local_worker_slice_time and\n // jobPerfData_measured_slice_time even when the worker is the only worker hooked\n // up to the DCP scheduler, we prove a little rounding. The rounding is 1/32 buckets.\n const discreteIncrement = 0.03125, discreteIncrementInverse = 32;\n\n /** @type {Object<string, number>} */\n const quanta = { 0: 1 };\n let averageLocalTime = 0, averageGlobalTime = 0;\n for (const jobMan of this.jobManagerInventory)\n {\n if (jobMan.emaSliceTime > 0 && jobMan.globalTime > 0)\n {\n quanta[jobMan.address] = jobMan.emaSliceTime;\n /** XXXpfr @todo Should we be using TotalTime here? */\n averageLocalTime += jobMan.emaSliceTime;\n averageGlobalTime += jobMan.globalTime;\n selectiveDebug2() && console.debug('jobQuanta: job state', this.dbg.sliceSandboxStr, `l-density/g-density, ${jobMan.estimateDensity}/${jobMan.metrics?.sliceCPUDensity}`, `local/global, ${jobMan.emaSliceTime}/${jobMan.globalTime}`);\n }\n }\n\n if (averageLocalTime && averageGlobalTime)\n {\n /** @todo XXXpfr Add 1 stddev? */\n // alpha=0.1 gives an effective period of 19\n const alpha = 0.1;\n this.localTime = nextEma(this.localTime, averageLocalTime, alpha);\n this.globalTime = nextEma(this.globalTime, averageGlobalTime, alpha);\n this.defaultQuanta = this.localTime / this.globalTime;\n\n // Discretize by discreteIncrement increments.\n this.defaultQuanta = (this.defaultQuanta > 1\n ? Math.floor(discreteIncrementInverse * this.defaultQuanta)\n : Math.ceil(discreteIncrementInverse * this.defaultQuanta)) * discreteIncrement;\n\n // Enforce reasonable cap and floor to keep things from getting too crazy.\n this.defaultQuanta = Math.min(Math.max(this.defaultQuanta, minQuanta), maxQuanta);\n\n // Fake jobAddress '0' to represent unknown jobs.\n quanta['0'] = this.defaultQuanta;\n }\n else\n this.defaultQuanta = 1.0;\n\n selectiveDebug() && console.debug(`jobQuanta: defaultQuanta ${quanta['0']}, this.localTime ${this.localTime}/${averageLocalTime}, this.globalTime ${this.globalTime}/${averageGlobalTime}, quanta:`, quanta);\n if (common.debugQuanta())\n {\n console.debug('localRawData:', this.dbg.localRawData);\n console.debug('localData:', this.dbg.localData);\n console.debug('globalData:', this.dbg.globalData);\n }\n return quanta;\n }\n\n /**\n * @todo XXXpfr Should we not schedule long slices to a worker with too low defaultQuanta?\n *\n * When the estimated time to completion of all work is more than\n * repoManMultiplier * targetTaskDuration * this.maxWorkingCores,\n * return slices until the excess is removed.\n * Be fair. Round-robin over all jobs until excess is eliminated.\n * Kill the long jobs 1st.\n */\n repoMan()\n {\n const threshold = this.options.repoManMultiplier * this.options.targetTaskDuration * this.maxWorkingCores;\n const workRemaining = this.workRemaining;\n let excess = workRemaining - threshold;\n selectiveDebug() && console.debug(`repoMan: excess ${excess}, workerRemaining ${workRemaining}, threshold ${threshold}`);\n if (excess > 0)\n {\n const slices = [];\n /** @param {JobManager[]} jmi */\n const returnFrom = (jmi) => {\n while (true)\n {\n const _excess = excess;\n for (const jobMan of jmi)\n {\n const _readySlices = jobMan.readySlices;\n if (_readySlices.length > 0)\n {\n const slice = _readySlices[0];\n slice.repoMan(); // Mark as FINISHED\n slices.push(slice);\n excess -= jobMan.adjSliceTime;\n if (excess <= 0)\n break;\n }\n }\n if (_excess === excess || excess <= 0)\n break;\n }\n }\n // Be fair. Round-robin over all jobs until excess is eliminated.\n // Except the long jobs are killed 1st.\n const longJobs = this.jobManagerInventory.filter((jobMan) => jobMan.emaSliceTime >= this.options.targetTaskDuration);\n if (longJobs.length > 0)\n returnFrom(longJobs);\n if (excess > 0)\n returnFrom(this.jobManagerInventory);\n selectiveDebug() && (slices.length > 0) && console.debug(`repoMan: excess ${excess}, workerRemaining ${workRemaining}, threshold ${threshold}, returned-slice-count ${slices.length}`);\n this.returnSlices(slices, 'repoMan');\n }\n }\n\n /**\n * Predict the total reduction in density of working sandboxes timeSpanMs from now.\n * This function is called right before fetchTask, in order to calculate how much space is available.\n * @returns {{ queued: Slice[], working: number }}\n */\n predictLoad()\n {\n const timeSpanMs = this.options.prefetchInterval\n const queued = [];\n let working = 0;\n for (const jobMan of this.jobManagerInventory)\n {\n const { queued: jmQueued, working: jmWorking } = jobMan.predictLoad(timeSpanMs);\n queued.push(...jmQueued);\n // Optimize to short-circuit when queued > 1, because we won't call fetchWork in that case.\n if (queued.length > 1)\n break;\n working += jmWorking;\n }\n selectiveDebug() && console.debug(`Supervisor.predictLoad: queued ${queued.length}/${this.queuedSlices.length}, working ${working}/${this.workingSlices.length}`)\n return { queued, working };\n }\n\n /**\n * On the first call to fetchTask\n * or when the last call to fetchTask found nothing,\n * or when there are no ready slices,\n * wait until at least 1 job is ready with at least 1 ready slice.\n * @param {Array<Promise<any>>} jobManagerPromises\n * @returns {Promise<any>}\n */\n waitUntilWorkIsReady (jobManagerPromises)\n {\n if (this.waitForWork)\n {\n debugging('supervisor') && console.debug(`waitUntilWorkIsReady: promise count ${jobManagerPromises?.length}`);\n this.waitForWork = false;\n // Promise.any is supported in Node 15, Chrome 85, Edge 85, Firefox 79, Safari 14, Opera 71.\n // It was implemented in node and browsers in 2nd half of 2020, so there's a good chance many\n // customers will not have browsers that support it. And currently (Jan. 2023) DCP uses node 14.\n return Promise_any(jobManagerPromises);\n }\n // Flush microtask queue\n return a$sleepMs(0);\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 return supShared.generateWorkerComputeGroups(this, this.dcp4.taskDistributor);\n }\n\n // _Idx\n //\n // availableSandboxSpace, fetchTask, addTaskToWorkload, fetchFromTD, clearUnusedJobManagersAndModuleCache\n //\n\n /**\n * Returns the number of unused sandbox slots to fill -- sent to fetchTask.\n * @param {Slice[]} queued\n * @param {number} working\n * @returns {number}\n */\n availableSandboxSpace (queued, working)\n {\n // If we find more than 1 queued slices, bail early.\n if (queued.length > 1)\n return 0; // We have more than 1 ready slices, no need to fetch.\n\n let longSliceCount = 0;\n if (queued.length < 1)\n this.waitForWork = true; // There are no ready slices.\n else if (queued[0].isLong)\n longSliceCount = 1;\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 roughly this.maxWorkingCores * 5-minutes.\n // However, there can only be this.maxWorkingCores # of long slices on a worker,\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 or is an estimation slice.)\n\n const numCores = this.maxWorkingCores - working - longSliceCount;\n selectiveDebug2() && console.debug('availableSandboxSpace', numCores, working, longSliceCount);\n return numCores;\n }\n\n /**\n * Ask the scheduler (task distributor) for work (Rq).\n * @param {object[]} [jobs=[]]\n * @returns {Promise<*>}\n */\n async fetchTask (jobs = [])\n {\n if (!this.isReady)\n return;\n\n const now = Date.now();\n const { queued, working } = this.predictLoad();\n const unusedFutureCoreSpace = this.maxWorkingCores - working;\n if (unusedFutureCoreSpace < common.doNotSchedule)\n {\n debugging('supervisor') && console.debug('fetchTask: There are no unused sandbox slots.', now - this.lastTime);\n return;\n }\n \n // Record fetch start time.\n this.fetchTaskStarted = now;\n\n // We check for pruning about every 25 seconds, or when must prune level is reached.\n if (this.sandboxInventory.length > this.options.mustPruneSandboxLevel\n || now > this.lastPrune + this.options.pruneFrequency)\n {\n this.lastPrune = now;\n this.pruneSandboxes();\n }\n\n // Every 60 seconds check to see if the estimated time to completion of all work is more than\n // repoManMultiplier * this.targetTaskDuration() * this.maxWorkingCores,\n // and then return slices until the excess is removed.\n // Be fair. Round-robin over all jobs until excess is eliminated. Kill the long jobs 1st.\n if (now > this.lastRepoMan + this.options.repoManFrequency)\n {\n this.lastRepoMan = now;\n this.repoMan();\n }\n\n // There are 2 barriers wrt fetchTask,\n // 1) fetchTaskBarrier is a barrier for the task fetching from task distributor path.\n // 2) roundRobinBarrier is a barrier for the slice execution path.\n\n try\n {\n const cpuSpaceToFill = this.availableSandboxSpace(queued, working);\n selectiveDebug2() && console.debug('Supervisor.fetchTask', cpuSpaceToFill, queued.length, working);\n if (cpuSpaceToFill < 1)\n {\n debugging('supervisor') && console.debug('Supervisor.fetchTask: Sufficient slices exist, so start executing.', now - this.lastTime, cpuSpaceToFill, queued.length, working);\n return this.roundRobinSlices();\n }\n selectiveDebug2() && console.debug('fetchTask begin q/w/slots/space/future-space', queued.length, working, this.unusedSandboxCount, this.unusedCoreSpace, unusedFutureCoreSpace);\n\n if (this.fetchTaskBarrier)\n return;\n // fetchTaskBarrier is a barrier for the task fetching from task distributor path.\n this.fetchTaskBarrier = true;\n\n /* @todo XXXpfr Think about how to do targetLoad.longSlices better.\n * Ideas:\n * 1) While branchy Javascript is CPU bound, that doesn't mean hyperthreading isn't useful.\n * When a branch is mispredicted the whole CPU instruction pipeline is flushed, which is\n * a huge perf hit and while waiting to fill the pipeline again, a hyperthread can get\n * a whole bunch of work done.\n * 2) Setting the Sup2.cores.cpu to #lCores is probably too much, but I've had great success with\n * Sup2.cores.cpu = dcpConfig.supervisor.tuning.coreRatio.cpu * #lCores\n * Which is very close to optimal throughput of work done.\n * 3) When the scheduler is 1/2 very long slices, the short slices will tend to get starved.\n * The config property dcpConfig.scheduler.preventSliceStarvation when set to true (default false)\n * will always leave one vCore open for short slices in every worker. In the future, I want the\n * scheduler to detect short slice starvation and dynamical turn preventSliceStarvation on until\n * short slice starvation is alleviated and then turn it back off.\n * 4) maxSandboxes is currently set to\n * factor * Sup2.cores.cpu\n * where 1.2 <= factor <= 1.5 depending upon how many lCores a worker has. Where I assume that a\n * machine with a large number of lCores has sufficient memory to handle a bigger factor.\n * The factor boundaries can be adjusted in dcpConfig.supervisor, but I intend to also allow them\n * to be overridden at the dcpConfig.worker level, so if somebody has a 32 lCore machine with\n * 8GB of RAM they can adjust factor to be closer to 1. Ideally we could adjust the factor\n * boundaries at the job/CG level.\n */\n\n const request = {\n supervisor: this.version,\n numCores: cpuSpaceToFill, /** @deprecated This is for legacy schedulers. */\n numGPUs: this.maxWorkingGPUs, /** @deprecated This is for legacy schedulers. */\n targetLoad: { cpu: cpuSpaceToFill, gpu: this.maxWorkingGPUs, longSlices: Math.floor(cpuSpaceToFill) },\n coreStats: this.options.getStatisticsCPU(),\n jobQuanta: this.jobQuanta(),\n capabilities: this.capabilities,\n paymentAddress: this.options.paymentAddress,\n jobAddresses: jobs.concat(this.options.jobAddresses || []), // When set, only fetches slices for these jobs.\n workerComputeGroups: this.generateWorkerComputeGroups(),\n 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(this.options.leavePublicGroup))\n request.workerComputeGroups.push(constants.computeGroups.public);\n\n debugging('supervisor') && console.debug('fetchTask is calling fetchFromTD', Date.now() - this.lastTime);\n\n // Call Task Distributor and handle response with this.addTaskToWorkload.\n return this.fetchFromTD(request, (response) => this.addTaskToWorkload(request, response));\n }\n catch (error)\n {\n this.fetchTaskBarrier = false;\n this.error('Supervisor.fetchTask failed!', error);\n }\n }\n\n /**\n * Callback for fetchFromTD.\n * @param {object} request\n * @param {object} response\n */\n async addTaskToWorkload (request, response)\n {\n const constructFetchHandle = (size, jobs, slices) => {\n return { \n fetchStart: this.fetchTaskStarted,\n fetchEnd: Date.now(),\n fetchSize: size,\n jobs,\n slices,\n };\n };\n\n try\n {\n /** @type {TDPayload} */\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 this.error(`Unable to request task from scheduler; will try again on a new connection: payload ${stringify(payload)}`);\n return;\n }\n\n if (!payload.body?.newJobs) // No slices found.\n {\n // Reset first fetch logic.\n this.waitForWork = true;\n /**\n * The 'fetch' event fires when the stask distributor found no work.\n * @link https://gitlab.com/Distributed-Compute-Protocol/dcp-docs-wes/-/blob/wip/worker/worker-events.md\n * @event Supervisor#fetch\n */\n this.workerEmit('fetch', constructFetchHandle(0, {}, {}));\n // There may be an extra slice to process.\n debugging('supervisor') && console.debug('Task distributor found no slices...');\n return this.roundRobinSlices();\n }\n\n /** @todo XXXpfr At this poin the line #'s are short by 42 -- figure out why. */\n\n /*\n * payload: { TDPayload }\n * TDPayload: { owner: Address, signature: Signature, auth: Auth, body: Body };\n * Auth: { workerId: string, authSlices: Object<string, SliceMessage[]>, schedulerId: { address: Address }, jobCommissions: Object<string, { rate: number, account: number }> }\n * Body: { newJobs: Object<string, object>, task: Object<string, SliceMessage[]>, computeGroupJobs: Object<string, string[]>, computeGroupOrigins: Object<string, Object<string, string[]>>, schedulerConfig: {{ targetTaskDuration: number }} }\n *\n * NOTE: authorizationMessage has type AuthMessage\n */\n\n const { body, ...authorizationMessage } = payload;\n const { newJobs, task, schedulerConfig } = body;\n const newJobKeys = Object.keys(newJobs);\n const jobCount = newJobKeys.length;\n\n let jobSliceMap = task;\n if (jobSliceMap.length) /** @deprecated Task came from legacy scheduler */\n // @ts-ignore\n jobSliceMap = toJobMap(task, sliceMsg => sliceMsg);\n\n if (schedulerConfig) // Otherwise the default is 300 seconds.\n this.options.targetTaskDuration = schedulerConfig.targetTaskDuration;\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 {\n // \"fetchTask:\" because that should make sense to somebody that doesn't know the internals of Supervisor.\n this.error(\"fetchTask: Worker received slices it shouldn't have; rejecting the work and stopping.\");\n this.stopWork(true);\n return;\n }\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(newJobs);\n\n /** @todo XXXpfr Figure out how not to construct this every time. */\n this.jobMap = {};\n this.jobManagerInventory.forEach(jobManager => {\n this.jobMap[jobManager.address] = jobManager;\n });\n\n selectiveDebug2() && console.debug(`addTaskToWorkload(${Date.now() - this.lastTime}): newJobs ${common.truncateAddress(newJobKeys)}, jobSliceMap ${common.compressJobMap(jobSliceMap, (s) => s.sliceNumber)}`);\n\n let sliceCount = 0;\n /** @type {Array<Promise<*>>} */\n const jobManagerPromises = [], jobs = {}, slices = {};\n // Populate the job managers with slices, creating new job managers when necessary.\n // Set up discrete job ring buffer.\n for (const [jobAddress, jobMessage] of Object.entries(newJobs))\n {\n /** @type {JobManager} */\n let jobManager;\n const sliceMessages = jobSliceMap[jobAddress];\n sliceCount += sliceMessages.length;\n\n if (this.jobMap.hasOwnProperty(jobAddress))\n {\n jobManager = this.jobMap[jobAddress];\n jobManager.update(jobMessage, sliceMessages, authorizationMessage);\n }\n else\n {\n // Add the slice messages to the job manager ctor, so that slice construction is after job manager is ready.\n jobManager = new JobManager(this, jobMessage, sliceMessages, authorizationMessage);\n this.jobMap[jobAddress] = jobManager;\n //this.jobManagerInventory.push(jobManager); DO NOT PUT IN INVENTORY UNTIL jobManager.ready .\n\n // Populate the ring buffer based on job's discrete property.\n if (jobMessage.requirements.discrete && this.ringBufferofJobs.find(address => address === jobAddress) === undefined)\n this.ringBufferofJobs.push(jobAddress);\n }\n jobs[jobAddress] = jobManager.jobHandle;\n slices[jobAddress] = task[jobAddress].length;\n\n jobManagerPromises.push(jobManager.jobPromise);\n }\n\n const payloadLength = kvin.stringify(payload).length; /** @TODO - fix per DCP-3750 */\n /**\n * The 'fetch' event fires when the supervisor has found work from the task distributor.\n * @link https://gitlab.com/Distributed-Compute-Protocol/dcp-docs-wes/-/blob/wip/worker/worker-events.md\n * @event Supervisor#fetch\n */\n this.workerEmit('fetch', constructFetchHandle(payloadLength, jobs, slices));\n\n const compressTask = () => { return common.compressJobMap(authorizationMessage.auth.authSlices); }\n selectiveSupEx() && console.debug(`addTaskToWorkload(${Date.now() - this.lastTime}): task: ${sliceCount}/${request.targetLoad.cpu}/${this.maxWorkingCores}, jobs: ${jobCount}, authSlices: ${compressTask()}, conversion:`, request.jobQuanta);\n\n // On the first call to fetchTask,\n // or when the last call to fetchTask found nothing,\n // or when there are no ready slices,\n // wait until at least 1 job with 1 slice is ready.\n await this.waitUntilWorkIsReady(jobManagerPromises);\n\n debugging('supervisor') && console.debug('addTaskToWorkload: Before calling roundRobinSlices; job states', this.jobManagerInventory.map((jm) => jm.identifier));\n\n // Start working on the new slices.\n return dcp_timers.setImmediate(() => this.roundRobinSlices());\n }\n catch (error)\n {\n this.workerEmit('fetch', error);\n this.error('Supervisor.fetchTask failed!', error);\n }\n finally\n {\n this.fetchTaskBarrier = false;\n }\n }\n\n /**\n * @private\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<any>}\n */\n async fetchFromTD (request, addTaskToWorkload)\n {\n selectiveDebug2() && console.debug('fetchFromTD begin; BarrierState:', this.fetchTaskBarrier, this.roundRobinBarrier);\n // Fetch a new task if we have insufficient slices queued, then start workers\n if (!this.fetchTaskBarrier)\n throw new Error('fetchTaskBarrier must be set when entering fetchFromTD.');\n\n this.dcp4.instantiateAllConnections();\n\n let fetchTimeout = dcp_timers.setTimeout(() => {\n this.fetchTaskBarrier = false;\n this.warning('Fetch exceeded timeout, will reconnect at next watchdog interval');\n this.dcp4.resetConnection('taskDistributor').catch(error => {\n this.error('Failed to close task-distributor connection', error);\n });\n this.dcp4.resetConnection('resultSubmitter').catch(error => {\n this.error('Failed to close result-submitter connection', error);\n });\n this.dcp4.instantiateAllConnections();\n }, 3 * 60 * 1000); // Max out at 3 minutes to fetch.\n // Allow workers and localExec to exit.\n fetchTimeout.unref();\n\n const finalize = () => {\n this.fetchTaskBarrier = false;\n if (fetchTimeout)\n dcp_timers.clearTimeout(fetchTimeout);\n fetchTimeout = null;\n }\n\n // Ensure result submitter and task distributor connections before fetching tasks.\n try\n {\n await Promise.all([\n this.dcp4.taskDistributor.keepalive(),\n this.dcp4.resultSubmitter.keepalive(),\n ]);\n }\n catch (error)\n {\n selectiveDebug() && console.debug('fetchTaskFromTD: Keep slices failed', error);\n this.warning('Failed to connect to result submitter, refusing to fetch slices.', 'Will try again at next fetch cycle.');\n this.dcp4.resetConnection('taskDistributor').catch(e => {\n this.error('Failed to close task-distributor connection', e);\n });\n this.dcp4.resetConnection('resultSubmitter').catch(e => {\n this.error('Failed to close result-submitter connection', e);\n });\n return finalize();\n }\n\n if (!this.dcp4.taskDistributor)\n {\n const msg = 'Unable to request task from scheduler; no connection to task distributor';\n this.warning(msg);\n this.workerEmit('fetch', new Error(msg));\n return finalize();\n }\n \n // The 'beforeFetch' event allows the user to cancel the requestTask request.\n let canceled = false;\n /**\n * The 'beforeFetch' event fires before the request is sent to requestTask in task distributor.\n * @link https://gitlab.com/Distributed-Compute-Protocol/dcp-docs-wes/-/blob/wip/worker/worker-events.md\n * @event Supervisor#beforeFetch\n */\n this.workerEmit('beforeFetch', () => { canceled = true; })\n selectiveDebug() && canceled && console.debug('User canceled the fetch task.');\n if (canceled)\n return finalize()\n\n return this.dcp4.taskDistributor.request('requestTask', request)\n .then((response) => {\n addTaskToWorkload(response);\n // Success! Restore this.dcp4.taskDistributor delay to retryMinSleepMs (currently 32ms.)\n // Is there a better way to reset than explicit calls?\n this.delayManager.resetEBO('taskDistributor');\n return response;\n })\n .catch((error) => {\n this.workerEmit('fetch', error);\n this.error('Unable to request task from scheduler. Will try again on a new connection.', error);\n this.dcp4.resetConnection('taskDistributor');\n })\n .finally(() => {\n return finalize();\n });\n }\n\n /**\n * Remove all unreferenced jobs in this.jobManagerInventory and this.moduleCache.\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<string, number[]>} newJobMap - Jobs that should not be removed from this.jobManagerInventory and this.moduleCache.\n */\n clearUnusedJobManagersAndModuleCache (newJobMap)\n {\n const emptyJobs = [];\n for (const jobMan of this.jobManagerInventory) // Grab oldest 1st\n {\n if (!newJobMap[jobMan.address])\n {\n let isEmpty = true;\n for (const slice of jobMan.sliceInventory)\n if (slice.isQueuedOrActive)\n {\n isEmpty = false;\n break;\n }\n if (isEmpty)\n {\n // Walk through whole list to purge empty jobs with no assigned sandboxes to save.\n if (jobMan.assignedSandboxes.length < 1)\n this.purgeJob(jobMan);\n else\n emptyJobs.push(jobMan)\n }\n }\n }\n let deleteCount = this.jobManagerInventory.length - this.options.cachedJobsThreshold;\n if (deleteCount > 0)\n {\n selectiveDebug() && console.debug(`Supervisor.clearUnusedJobManagersAndModuleCache: deleteCount ${deleteCount}/${this.jobManagerInventory.length}/${this.options.cachedJobsThreshold}.`);\n for (const jobMan of emptyJobs) // Grab oldest 1st\n {\n this.purgeJob(jobMan);\n if (--deleteCount < 1)\n break;\n }\n }\n }\n\n // _Idx\n //\n // createSandbox, returnSandbox, hookUpSandboxListeners, pruneSandboxes\n //\n\n /**\n * Automatically handle when the evaluator is down.\n *\n * With the screensaver worker, when the screensaver goes down, so does the evaluator.\n * And when the screensaver starts running again, so does the evaluator. The evaluator\n * may be stopped and started again with sa worker running, and have good behavior.\n * However, browser workers cannot have their evaluators stopped without also stopping\n * the worker (otherwise file-a-bug...)\n *\n * @param {boolean} [throwError=false]\n * @returns {Promise<Sandbox>}\n */\n async createSandbox (throwError = false)\n {\n selectiveDebug2() && console.debug('createSandbox', this.sandboxInventory.length, Date.now() - this.lastTime);\n // See if there are any READY_FOR_ASSIGN sandboxes (viz., sandbox.isReadyForAssign is true.)\n // If the evaluator just came back up (while worker is still running) there should not be any non-assigned sandboxes.\n // We're only considering sa worker (e.g. screensaver worker), because browser workers cannot stop the\n // evaluator w/o stopping the worker (I think -- if not true, file-a-bug.)\n if (this.sandboxInventory.length > 0 && this.sandboxInventory[0].isReadyForAssign)\n {\n selectiveDebug2() && console.debug(`Supervisor.createSandbox: Found ready-for-assign sandbox ${this.sandboxInventory[0].identifier}`);\n return this.sandboxInventory.shift();\n }\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 try\n {\n this.evaluator.createSandboxRefCount++;\n\n let retry = 0;\n while (true)\n {\n let sandbox;\n try\n {\n sandbox = new Sandbox(this, { ...this.options.sandboxOptions });\n selectiveDebug2() && console.debug(`Supervisor.createSandbox(${sandbox.id}): Calling sandbox.start: ${this.evaluator.createSandboxRefCount}, eval-down ${this.evaluator.down}`);\n this.hookUpSandboxListeners(sandbox);\n await sandbox.start();\n if (!this.capabilities)\n this.checkCapabilities(sandbox);\n if (this.evaluator.reallyDown)\n {\n this.evaluator.reallyDown = false;\n selectiveDebug() && console.debug('Supervisor.createSandbox: Evaluator is up again.', this.evaluator.createSandboxRefCount);\n this.jobManagerInventory.forEach((jobManager) => jobManager.resetSlices('createSandbox'));\n }\n return sandbox;\n }\n catch (error)\n {\n if (throwError)\n throw error;\n selectiveDebug() && console.debug(`Supervisor.createSandbox: Failed to start sandbox ${sandbox.identifier}`, this.evaluator.createSandboxRefCount, this.evaluator.down, error.message);\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\n if (throwError)\n throw error;\n\n // The evaluator may be down or shutting down, keep retrying.\n if ((retry % 60) === 0)\n this.warning('Failed to start a sandbox; will keep retrying; screensaver worker or evaluator may be down...');\n await a$sleepMs(1000 * Math.min(5, ++retry));\n }\n }\n }\n finally\n {\n this.evaluator.createSandboxRefCount--;\n }\n }\n\n /**\n * Remove sandbox from inventory and terminate.\n * @param {Sandbox} sandbox\n */\n returnSandbox (sandbox)\n {\n // If sandbox is not in this.sandboxInventory then sandbox must already be terminated\n // <==> this.sandboxInventory.includes(sandbox) || sandbox.isTerminated().\n selectiveDebug2() && console.debug(`returnSandbox: ${sandbox.identifier}`);\n if (common.removeElement(this.sandboxInventory, sandbox))\n sandbox.terminate(false);\n else\n {\n // If sandbox is not in this.sandboxInventory then sandbox must already be terminated\n if (common.displayMaxDiagInfo() && !sandbox.isTerminated) // Design assumption.\n throw new Error(`returnSandbox: Sandbox ${sandbox.identifier} has already been removed.`);\n }\n }\n\n /**\n * For a given sandbox, hook up all the Sandbox listeners.\n * @param {Sandbox} sandbox\n */\n hookUpSandboxListeners (sandbox)\n {\n sandbox.addListener('start', () => {\n if (!sandbox.slice) return;\n const payload = sandbox.slice.getMessagePayload(this.workerId, 'begin');\n return this.dcp4.safeRSStatus(payload, `Failed to send 'begin' status for slice ${sandbox.slice.identifier}`);\n });\n\n const that = this;\n // Sandbox error handler.\n sandbox.on('sandboxError', function Supervisor$sandboxError(error) {\n selectiveDebug() && console.debug(`Sandbox ${sandbox.identifier} sandboxError-handler; error while executing work function`, error);\n const slice = sandbox.slice;\n if (!slice?.isWorking) // Sanity -- warning should never fire.\n this.warning(`handleSandboxError: slice ${slice?.identifier} must be WORKING.`);\n if (slice)\n slice['useRetryLogic'] = true;\n that.returnSandbox(sandbox);\n });\n\n // Sandbox complete handler.\n // When any sandbox completes, go through the Supervisor.fetchTask protocol.\n sandbox.addListener('complete', () => {\n // Try not to call fetchTask unless there's something there.\n selectiveDebug2() && console.debug('Sandbox complete listener', this.fetchTaskBarrier, this.roundRobinBarrier, this.unusedSandboxCount, Date.now() - this.lastTime);\n if (!this.fetchTaskBarrier)\n this.fetchTask();\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.sandboxHandle.on('end', async () => {\n if (this.sandboxInventory.length > 0 && !this.evaluator.pauseSandboxHandleEndHandler)\n {\n selectiveDebug() && console.debug(`hookUpSandboxListeners: Sandbox \"${sandbox.identifier}\" terminated handler`, this.sandboxInventory.length, Date.now() - this.lastTime);\n\n // Does there exist a non-terminated sandbox?\n let allSandboxesTerminated = true;\n for (const sbx of this.sandboxInventory)\n if (!sbx.isTerminated)\n {\n allSandboxesTerminated = false;\n break;\n }\n\n if (allSandboxesTerminated && !this.evaluator.downInterlock)\n {\n //\n // When we get here, all sandboxes have been terminated.\n //\n this.evaluator.downInterlock = true;\n selectiveDebug() && console.debug('hookUpSandboxListeners: Try to create 1 sandbox in the sandbox-terminated-handler...', sandbox.identifier);\n await this.createSandbox(true /*throwError*/)\n .then((sbx) => {\n this.evaluator.reallyDown = false;\n // This is the only place where non-assigned sandboxes are added to this.sandboxInventory.\n this.sandboxInventory.unshift(sbx);\n selectiveDebug() && console.debug('Sandbox terminate handler was able to create new sandbox', sandbox.identifier);\n })\n .catch(() => {\n //\n // Since all sandboxes have been terminated, if we cannot create a new sandbox,\n // that probably means we're on a screensaver worker and the screensaver is down.\n // Try to submit results for completed slices, but return all other non-finished\n // slices to the scheduler -- after a brief delay.\n //\n selectiveDebug() && console.debug('Sandbox terminate handler cannot create new sandbox; evaluator is down', sandbox.identifier);\n this.evaluator.reallyDown = true;\n this.emit('evalDown');\n const delay = 60; // seconds\n this.jobManagerInventory.forEach((jm) => jm.evaluatorDownCleanup(delay));\n this.warning('Stopping all work.', 'Screensaver worker or evaluator may be down.');\n })\n .finally(() => {\n this.sandboxInventory = this.sandboxInventory.filter(sbx => !sbx.isTerminated);\n this.evaluator.shuttingDown = false;\n this.evaluator.downInterlock = false;\n });\n }\n }\n });\n }\n\n /**\n * Terminate extra sandboxes over the limit.\n * @todo XXXpfr Prioritize keeping expensive to assign sandboxes.\n */\n pruneSandboxes ()\n {\n this.sandboxInventory = this.sandboxInventory.filter((sandbox) => !sandbox.isTerminated);\n let pruneCount = this.sandboxInventory.length - this.options.maxSandboxes;\n if (pruneCount <= 0)\n return;\n\n selectiveDebug() && console.debug(`Supervisor.pruneSandboxes START: pruneCount ${pruneCount}/${this.sandboxInventory.length}/${this.options.maxSandboxes}.`, this.dbg.dumpSandboxState());\n selectiveDebug2() && console.debug(this.sandboxInventory.map((sbx) => sbx.identifier));\n\n // Prune ready-for-assign sandboxes first.\n while (pruneCount > 0)\n {\n if (this.sandboxInventory[0].isReadyForAssign)\n {\n const startedSandbox = this.sandboxInventory.shift();\n startedSandbox.terminate(false);\n pruneCount--;\n }\n else\n break;\n }\n\n // Don't purge jobs here: can accidentally purge a job that TD just fetched (XXXpfr)\n\n /**\n * Do we really want to do a bunch of work to keep empty job assigned sandboxes around?\n * When in a private compute group, there will be fewer jobs and it's likely\n * that a given job will be seen again.\n * @todo XXXpfr Prioritize keeping expensive to assign sandboxes.\n */\n const liveJobs = [], emptyJobs = [];\n let maxAssignedSandboxCount = 0;\n for (const jobMan of this.jobManagerInventory)\n {\n let isAlive = false;\n for (const slice of jobMan.sliceInventory)\n if (slice.isQueuedOrActive)\n {\n isAlive = true;\n break;\n }\n if (isAlive)\n liveJobs.push(jobMan);\n else\n {\n const _assignedSandboxes = jobMan.assignedSandboxes;\n if (maxAssignedSandboxCount < _assignedSandboxes.length)\n maxAssignedSandboxCount = _assignedSandboxes.length;\n emptyJobs.push(jobMan);\n }\n }\n\n if (emptyJobs.length > 0)\n {\n // Prune the sandboxes from all jobs with no current work.\n // Try to keep approximately the same # of assigned sandboxes per job.\n for (let k = maxAssignedSandboxCount; k >= 0; k--)\n {\n for (const jobMan of emptyJobs)\n {\n const _assignedSandboxes = jobMan.assignedSandboxes;\n if (_assignedSandboxes.length > k)\n {\n debugging('supervisor') && console.debug(`pruneSandboxes(empty): sandbox${_assignedSandboxes[0].id}`, Date.now() - this.lastTime);\n // Terminate and remove from this.sandboxInventory.\n this.returnSandbox(_assignedSandboxes[0]);\n if (--pruneCount < 1)\n {\n debugging('supervisor') && console.debug(`Supervisor.pruneSandboxes FINISH: ${pruneCount}/${this.sandboxInventory.length}/${this.options.maxSandboxes}.`);\n return;\n }\n }\n }\n }\n }\n\n // Round-robin prune 1 extra assigned sandbox from each non-empty jobmanager.\n while (pruneCount > 0)\n {\n const _pruneCount = pruneCount;\n for (const jobMan of liveJobs)\n {\n const _assignedSandboxes = jobMan.assignedSandboxes;\n if (_assignedSandboxes.length > 0)\n {\n debugging('supervisor') && console.debug(`pruneSandboxes(non-empty): sandbox${_assignedSandboxes[0].id}`, Date.now() - this.lastTime);\n // Terminate and remove from this.sandboxInventory.\n this.returnSandbox(_assignedSandboxes[0]);\n if (--pruneCount < 1)\n {\n debugging('supervisor') && console.debug(`Supervisor.pruneSandboxes FINISH: ${pruneCount}/${this.sandboxInventory.length}/${this.options.maxSandboxes}.`);\n return;\n }\n }\n }\n if (_pruneCount === pruneCount) // Nothing left to prune.\n break;\n }\n\n debugging('supervisor') && console.debug(`Supervisor.pruneSandboxes FINISH: incomplete-prune ${pruneCount}/${this.sandboxInventory.length}/${this.options.maxSandboxes}.`);\n }\n\n // _Idx\n //\n // recordResult, sendToResultSubmitter, sendResultToRemote\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 * @param {Sandbox} sandbox - The sandbox handle associated to the slice.\n * @returns {Promise<any>}\n */\n recordResult (slice, sandbox)\n {\n // It is possible for slice.result to be undefined when there are upstream errors.\n if (!slice.result)\n throw new Error(`Slice ${slice.identifier} completed work, but there is no result. This is ok when there are upstream errors.`);\n if (!slice.isComplete)\n throw new Error(`Cannot record result for slice ${slice.identifier} that has not completed execution successfully.`);\n if (!slice.timeReport)\n throw new Error(`Invalid time report for slice ${slice.identifier} in recordResult`);\n if (!slice.dataReport)\n throw new Error(`Invalid data report for slice ${slice.identifier} in recordResult`);\n\n const metrics = slice.jobManager.updateStatistics(slice, sandbox);\n selectiveDebug() && console.debug(`Supervisor: recording result for slice ${slice.identifier} with metrics`, this.dbg.justCPU(metrics));\n\n /** @see result-submitter::result for full message details */\n const payloadData = {\n slice: slice.sliceNumber,\n job: slice.jobAddress,\n worker: this.workerId,\n paymentAddress: this.options.paymentAddress,\n metrics,\n authorizationMessage: slice.authorizationMessage,\n };\n\n let canceled = false;\n const resultUrl = (slice.resultStorageType !== 'pattern') ? slice.resultStorageDetails : false;\n this.workerEmit( 'beforeResult', () => { canceled = true; }, resultUrl);\n this.jobEmit(slice, 'beforeResult', () => { canceled = true; }, resultUrl);\n selectiveDebug && canceled && console.debug(`User canceled the result submission operation for slice ${slice.identifier}.`);\n if (canceled)\n return this.returnSlice(slice, 'Canceled via beforeResult event');\n\n if (slice.resultStorageType === 'pattern')\n return this.sendResultToRemote(slice)\n .then((response) => {\n return this.sendToResultSubmitter(slice, sandbox.sandboxHandle, payloadData, response);\n });\n\n return this.sendToResultSubmitter(slice, sandbox.sandboxHandle, payloadData, encodeDataURI(slice.result.result));\n }\n\n /**\n * Send result to result submitter.\n * @param {Slice} slice\n * @param {SandboxHandle} sandboxHandle\n * @param {*} payloadData\n * @param {string} [result]\n * @returns {Promise<any>}\n */\n async sendToResultSubmitter (slice, sandboxHandle, payloadData, result)\n {\n // When handleRSError is hit, { slice, payload } is added to the queue this.dcp4.submitResultsQueueMap[slice.key] .\n // For a given slice, the queue is retried independent of other slices that failed to submit.\n // When a given slice hits the retry limit (6 retries) the slice is returned to scheduler.\n const handleRSError = (error, slice, payloadData) => { // eslint-disable-line no-shadow\n const msg = `Failed to submit results to scheduler for slice ${slice.identifier}`;\n if (!error) error = new Error(msg);\n this.error(msg, error);\n\n slice['retrySubmitResults'] = (slice['retrySubmitResults'] ?? 0) + 1;\n if (slice['retrySubmitResults'] > this.options.maxResultSubmissionRetries)\n {\n this.handleFailedSlice(slice, error);\n throw new Error(`Failed to submit results 6 times for slice ${slice.identifier}`);\n }\n\n // For a given slice, there's never more than one element in the corresponding queue.\n this.dcp4.submitResultsQueueMap[slice.key] = [ { slice, sandboxHandle, payloadData } ];\n return this.dcp4.resetConnection('resultSubmitter');\n }\n\n try\n {\n debugging('supervisor') && console.debug('Supervisor.recordResult: payloadData', result.slice(0, 256), slice.identifier);\n if (result)\n payloadData.result = result;\n\n await this.delayManager.nextDelay('recordResult', 2);\n //->console.log('recordResult', slice.identifier, this.evaluator.down, Date.now() - this.lastTime); // SAVE\n\n return this.dcp4.resultSubmitter.request('result', payloadData)\n .then((resp) => {\n const payload = resp.payload;\n if (!resp.success)\n {\n if (payload)\n {\n selectiveDebug() && console.debug('resultSubmitter.send failed', payload);\n throw new DCPError(`Call to result submitter failed when recording results for ${slice.identifier}.`, payload);\n }\n if (debugBuild)\n {\n selectiveDebug() && console.debug('resultSubmitter.send failed with no payload', slice.identifier);\n // Look inside\n for (const [ key, value ] of Object.entries(resp)) {\n if (key !== 'connection')\n console.debug(`${key}:`, value);\n }\n }\n throw new Error(`Call to result submitter failed when recording results for ${slice.identifier}.`);\n }\n\n debugging('supervisor') && console.debug('Successfully submitted results', slice.identifier);\n\n // Success! Restore this['resultSubmitter'] delay to retryMinSleepMs (currently 32ms.)\n // Is there a better way to reset than explicit calls?\n this.delayManager.resetEBO('resultSubmitter');\n\n common.debugQuanta() && this.dbg.addGlobal(slice, payload.metrics);\n slice.jobManager.update({ metrics: payload.metrics }); // Update metrics\n\n // Emit the 3 'payment' events.\n const paymentAddress = payloadData.paymentAddress.toString();\n this.workerEmit( 'payment', payload.slicePaymentAmount, paymentAddress, slice.jobAddress, slice.sliceNumber);\n this.jobEmit(slice, 'payment', payload.slicePaymentAmount, paymentAddress, slice.sliceNumber);\n this.safeEmit(sandboxHandle, 'payment', payload.slicePaymentAmount, paymentAddress);\n\n const payloadLength = kvin.stringify(payloadData).length; /** @TODO - fix per DCP-3750 */\n const resultUrl = (slice.resultStorageType !== 'pattern') ? slice.resultStorageDetails : false;\n this.workerEmit( 'result', resultUrl, payloadLength);\n this.jobEmit(slice, 'result', resultUrl, payloadLength);\n\n slice.markAsFinished();\n\n // Remove the slice from the job manager.\n slice.jobManager.removeSlice(slice);\n\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}.`, Date.now() - this.lastTime);\n }\n if (false)\n {}\n\n return resp;\n })\n .catch ((error) => {\n handleRSError (error, slice, payloadData);\n });\n }\n catch (error)\n {\n handleRSError (error, slice, payloadData);\n }\n }\n\n /**\n * Send a work function's result to a server that speaks our DCP Remote Data Server protocol.\n * E.g. https://gitlab.com/Distributed-Compute-Protocol/dcp-rds\n *\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 */\n sendResultToRemote (slice)\n {\n return supShared.sendResultToRemote(this, slice);\n }\n\n // _Idx\n //\n // handleWorkReject\n //\n\n /**\n * Handles reassigning or returning a slice that rejected.\n *\n * If error.message === 'false' and slice.hasBeenRejected is false, reschedule the slice.\n * Set the slice.hasBeenRejected to be true.\n *\n * If error.message !== 'false' or slice.hasBeenRejected is true (i.e. has been rejected once already)\n * zthen return all slices from the job to the scheduler and terminate all sandboxes with that jobAddress.\n *\n * @param {Slice} slice\n * @param {Error} error\n */\n handleWorkReject (slice, error)\n {\n debugging() && console.debug('handleWorkReject', error.message, slice.hasBeenRejected, slice.identifier);\n\n const jobManager = slice.jobManager;\n jobManager.rejectedJobReasons.push(error.message); // memoize reasons\n\n // First time rejecting without a reason; try rescheduling the slice.\n if (error.message === 'false' && !slice.hasBeenRejected)\n {\n // Mark slice as rejected.\n slice.hasBeenRejected = true;\n // Reset slice state to allow re-execution.\n slice.resetState();\n }\n else\n {\n // Slice has been rejected twice, so add to array of rejected jobs.\n const rejectedJob = {\n address: slice.jobAddress,\n reasons: jobManager.rejectedJobReasons,\n };\n this.rejectedJobs.push(rejectedJob);\n // Broadcast failure.\n this.workerEmit( 'result', error);\n this.jobEmit(slice, 'result', error);\n // Purge the job.\n this.purgeJob(jobManager);\n // Tell everyone all about it, when allowed.\n if (jobManager.displayMaxDiagInfo)\n {\n const suffixMsg = 'All slices and sandboxes with the same jobAddress returned to the scheduler or terminated.';\n if (slice.hasBeenRejected)\n this.warning(`work.reject: The slice ${slice.identifier} was rejected twice.`, suffixMsg);\n else\n this.warning(`work.reject: The slice ${slice.identifier} was rejected with reason: ${error.message}.`, suffixMsg);\n }\n }\n }\n\n}\nexports.Supervisor = Supervisor;\n\n\n//# sourceURL=webpack://dcp/./src/dcp-client/worker/supervisor2/index.js?");
|
|
4728
4728
|
|
|
4729
4729
|
/***/ }),
|
|
4730
4730
|
|
|
@@ -4735,7 +4735,7 @@ eval("/**\n * @file dcp/src/dcp-client/worker/supervisor2/index.js\n *
|
|
|
4735
4735
|
/***/ ((__unused_webpack_module, exports, __webpack_require__) => {
|
|
4736
4736
|
|
|
4737
4737
|
"use strict";
|
|
4738
|
-
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@distributive.network,\n * Paul, paul@distributive.network,\n * @date Dec 2020,\n * June 2022, Jan-April 2023\n * @module job-manager\n * @copyright Copyright (c) 2018-2023, Distributive Corp. All Rights Reserved\n */\n// @ts-check\n\n\n/** @typedef {string} opaqueId */ // 22 character base64 string\n/** @typedef {string} address */ // String(Address)\n/** @typedef {import('./sandbox2').Sandbox} Sandbox */\n/** @typedef {import('./index').Supervisor} Supervisor */\n/** @typedef {import('dcp/utils/jsdoc-types').SliceMessage} SliceMessage */\n/** @typedef {import('dcp/utils/jsdoc-types').Auth} Auth */\n/** @typedef {import('dcp/utils/jsdoc-types').Signature} Signature */\n/** @typedef {import('dcp/utils/jsdoc-types').AuthMessage} AuthMessage */\n/** @typedef {import('dcp/dcp-client/worker/origin-access-manager').OriginAccessManager} OriginAccessManager */\n\nconst inspect = Symbol.for('nodejs.util.inspect.custom');\nconst debugging = (__webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope)('worker');\nconst EventEmitter = __webpack_require__(/*! events */ \"./node_modules/events/events.js\");\nconst { Inventory } = __webpack_require__(/*! dcp/utils/inventory */ \"./src/utils/inventory.js\");\nconst { Synchronizer } = __webpack_require__(/*! dcp/common/concurrency */ \"./src/common/concurrency.js\");\nconst { rehydrateRange } = __webpack_require__(/*! dcp/dcp-client/range-object */ \"./src/dcp-client/range-object.js\");\nconst { fetchURIAndLength } = __webpack_require__(/*! dcp/utils/fetch-uri */ \"./src/utils/fetch-uri.js\");\nconst utils = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\nconst { stringify, a$sleepMs, constructAlpha, nextEma } = utils;\nconst { allowOriginsPurposes } = __webpack_require__(/*! dcp/common/worker-constants */ \"./src/common/worker-constants.js\");\n\nconst { Slice } = __webpack_require__(/*! ./slice2 */ \"./src/dcp-client/worker/supervisor2/slice2.js\");\nconst { Promise_any } = __webpack_require__(/*! ./promise_any */ \"./src/dcp-client/worker/supervisor2/promise_any.js\");\nconst { Statistics } = __webpack_require__(/*! ./rolling-statistics */ \"./src/dcp-client/worker/supervisor2/rolling-statistics.js\");\nconst { SandboxError, UncaughtExceptionError } = __webpack_require__(/*! ./sandbox2 */ \"./src/dcp-client/worker/supervisor2/sandbox2.js\");\nconst { updateGPUStatistics } = __webpack_require__(/*! ./gpu_support */ \"./src/dcp-client/worker/supervisor2/gpu_support.js\");\nconst common = __webpack_require__(/*! ./common */ \"./src/dcp-client/worker/supervisor2/common.js\");\nconst { selectiveDebug, selectiveDebug2, debugBuild } = common;\n\nconst INITIAL = 'initial';\nconst READY = 'ready';\nconst STOP = 'stop';\nconst REFUSE = 'refuse';\nconst BROKEN = 'broken';\n\nclass BadOriginError extends SandboxError { constructor(msg) { super('EPERM_ORIGIN', msg); } }\nclass RemoteFetchError extends SandboxError { constructor(msg) { super('EFETCH', msg); } }\n\n/**\n * Public event emitter.\n * https://gitlab.com/Distributed-Compute-Protocol/dcp-docs-wes/-/blob/wip/worker/worker-events.md\n */\nclass JobHandle extends EventEmitter\n{\n /** @type {JobManager} */\n #jobManager;\n \n /**\n * @constructor\n * @param {JobManager} jobManager\n */\n constructor (jobManager)\n {\n super({ captureRejections: false });\n this.#jobManager = jobManager;\n }\n /** @type {string} */\n get address () { return this.#jobManager.address; }\n /** @type {string} */\n get name () { return this.#jobManager.public ? this.#jobManager.public.name : '<unknown>'; }\n /** @type {string} */\n get description () { return this.#jobManager.public ? this.#jobManager.public.description : '<unknown>'; }\n /** @type {string} */\n get link () { return this.#jobManager.public ? this.#jobManager.public.link : '<unknown>'; }\n}\nexports.JobHandle = JobHandle;\n\n//\n// Index to functionality -- search for '_Idx' to toggle through the index.\n//\n// 1) class JobManager\n// 2) addSlices, fetchDependencies, updateStatistics, update, predictLoad\n// 3) reserveOneSlice, resetSlices, destroy, evaluatorDownCleanup\n// 4) assignSandbox, runSlice, runSliceOnSandbox\n// 5) removeSlice\n// 6) fetchError, fetchJob, fetchSliceData\n// 7) jobEmit, sandboxEmit, error, warning. countSliceStr, dumpSlices.\n//\n\n// _Idx\n//\n// class JobManager\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 * initial -> 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\n{\n /** @type {Supervisor} */\n #supervisor;\n /** @type {Slice[]} */\n #sliceInventory;\n /** @type {Synchronizer} */\n #state;\n /** @type {*} */ // Figure out the type\n #jobMessage;\n /** @type {string} */\n #address;\n /** @type {Statistics} */\n #statistics;\n /** @type {Promise<*>} */\n #jobPromise;\n\n /**\n * @constructor\n * @param {Supervisor} parent - Owning Supervisor.\n * @param {object} jobMessage - Job Descriptor from getJobsForTask.\n * @param {SliceMessage[]} sliceMessages - Messages from task distributor describing slices.\n * @param {AuthMessage} authorizationMessage - The signature that shipped with the task authorizing this worker.\n */\n constructor(parent, jobMessage, sliceMessages, authorizationMessage)\n {\n this.#supervisor = parent;\n this.#sliceInventory = []; // All slices for this.address.\n this.#state = new Synchronizer(INITIAL, [ INITIAL, READY, STOP, REFUSE, BROKEN ]);\n this.#jobMessage = { ...jobMessage };\n this.#address = String(this.jobMessage.address);\n this.#statistics = new Statistics(0.5); // Effective period of 12 or 13\n this.#jobPromise = null;\n\n /** @type {{CPUTime: number, GPUTime: number, TotalTime: number, CPUDensity: number, GPUDensity: number, InDataSize: number, OutDataSize: number }} */\n this.localMetrics = null;\n /** @type {string[]} */\n this.rejectedJobReasons = [];\n /** @type {boolean} */\n this.isEstimation = true;\n /** @type {number} */\n this.inputDataSize = 0;\n /**\n * jobMan.evaluatorDownCleaup returns this.evaluatorDownHandle=a$sleepMs which is\n * interruptible by calling this.evaluatorDownHandle.intr().\n * @type {Promise<void>}\n */\n this.evaluatorDownHandle = null;\n\n /*\n * Make sure this.metrics contains valid data.\n * With old legacy schedulers, this may not be so.\n */\n this.fixUpMetrics();\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 for work.\n */\n this.#jobPromise = this.fetchDependencies(sliceMessages, authorizationMessage);\n\n /* Check for GPU support. */\n const _env = this.requirements?.environment;\n /** @type {boolean} */\n this.useGPU = _env ? (_env.webgpu || _env.offscreenCanvas) : false;\n \n /**\n * Event emitter containing info that describes the job.\n * @type {JobHandle}\n */\n this.jobHandle = new JobHandle(this);\n\n /**\n * Emit the 'job' event on the worker event emitter.\n */\n this.supervisor.safeEmit(this.supervisor.worker, 'job', this.jobHandle);\n }\n\n /** @type {*} */ // Figure out the type\n get jobMessage () { return this.#jobMessage; }\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 /** @type {boolean} */\n get almostDone () { return this.jobMessage.almostDone; }\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, sliceInDataSize: number, sliceOutDataSize: number, lastSliceNumber: number, measuredSlices: number, alpha: 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 ready () { return this.state.is(READY); }\n\n // These 4 aren't needed yet.\n ///** @type {boolean} */\n //get initial () { return this.state.is(INITIAL); }\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.supervisor.sandboxInventory.filter((sandbox) => sandbox.jobAddress === this.#address && !sandbox.isTerminated); }\n /** @type {Sandbox[]} */\n get assignedSandboxes () { return this.supervisor.sandboxInventory.filter((sandbox) => sandbox.jobAddress === this.#address && sandbox.isAssigned); }\n /** @type {Sandbox[]} */\n get workingSandboxes () { return this.supervisor.sandboxInventory.filter((sandbox) => sandbox.jobAddress === this.#address && sandbox.isWorking); }\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[]} */\n get queuedSlices () { return this.sliceInventory.filter((slice) => slice.isQueued); } // Ready and soon-to-be-ready\n /** @type {Slice[]} */\n get reservedSlices () { return this.sliceInventory.filter((slice) => slice.isReserved); }\n /** @type {Slice[]} */\n get activeSlices () { return this.sliceInventory.filter((slice) => slice.isActive); } // Reserved, working, workdone and complete\n /** @type {Slice[]} */\n get workingSlices () { return this.sliceInventory.filter((slice) => slice.isWorking || slice.isReserved); } // Working and soon-to-be-working\n /** @type {Slice[]} */\n get unFinishedSlices () { return this.sliceInventory.filter((slice) => !slice.isFinished); }\n\n /** @type {Statistics} */\n get statistics () { return this.#statistics; }\n /** @type {Promise<*>} */\n get jobPromise () { return this.#jobPromise; }\n /** @type {number} */\n get globalTime () { return utils.globalTime(this.metrics); }\n /** @type {number} */\n get emaSliceTime () { return this.localMetrics ? utils.localTime(this.localMetrics) : (utils.globalTime(this.metrics) * this.supervisor.defaultQuanta); }\n /** @type {number} */\n get emaTotalTime () { return this.localMetrics?.TotalTime || (utils.globalTotalTime(this.metrics) * this.supervisor.defaultQuanta); }\n /** @type {number} */\n get adjSliceTime () { return Math.min(this.emaSliceTime, this.supervisor.options.targetTaskDuration); }\n /** @type {string} */\n get identifier () { return `${common.truncateAddress(this.address)}.${this.state}`; }\n /** @type {string} */\n get sliceState () { return `${this.queuedSlices.length}.${this.workingSliceCount}.${this.sliceInventory.length}.${this.workRemaining}`; }\n /** @type {string} */\n get [inspect] () { return `[Object JobManager <${this.public.name}::${common.truncateAddress(this.address)}::${this.state}>]`; }\n /** @type {OriginAccessManager} */\n get originManager() { return this.supervisor.originManager; }\n /**\n * Expected density of a slice.\n * When we get the GPU stuff codified, the definition might be\n * { cpu: this.localMetrics?.CPUDensity ?? 1.0, gpu: this.localMetrics?.GPUDensity ?? 1.0}\n * @type {number}\n */\n get estimateDensity () { return this.localMetrics?.CPUDensity || utils.globalDensity(this.metrics); }\n /** @type {number} */\n get estimateGPUDensity () { return this.localMetrics?.GPUDensity || utils.globalGPUDensity(this.metrics); }\n /** @type {number} */\n get cpuTime () { return this.localMetrics?.CPUTime || this.metrics?.sliceCPUTime || 0; }\n /**\n * Estimate the number of milliseconds of total execution time of the job.\n * @type {number}\n */\n get workRemaining ()\n {\n const _adjSliceTime = this.adjSliceTime;\n const queuedTime = _adjSliceTime * this.queuedSlices.length;\n let workingTime = 0;\n for (const slice of this.workingSlices)\n {\n workingTime += Math.max(0, _adjSliceTime - (Date.now() - slice.startTime));\n selectiveDebug2() && console.debug(`JobManager.workRemaining: _adjSliceTime ${_adjSliceTime}, slice-time-elapsed ${Date.now() - slice.startTime}`);\n }\n selectiveDebug() && console.debug(`JobManager.workRemaining: queuedTime ${queuedTime}, queuedCount ${this.queuedSlices.length}, workingTime ${workingTime}, workingCount ${this.workingSliceCount}`);\n return workingTime + queuedTime;\n }\n /**\n * Are there no working slices?\n * @type {boolean}\n */\n get isNotWorking ()\n {\n for (const slice of this.sliceInventory)\n if (slice.isReserved || slice.isWorking)\n return false;\n return true;\n }\n /**\n * Estimated count of working (or reserved) slices.\n * @type {number}\n */\n get workingSliceCount ()\n {\n let count = 0;\n for (const slice of this.sliceInventory)\n if (slice.isReserved || slice.isWorking)\n ++count;\n return count;\n }\n /**\n * Estimated density of working (or reserved) slices.\n * @type {number}\n */\n get workingSliceDensity () { return this.workingSliceCount * this.estimateDensity; }\n /**\n * Estimated density of working (or reserved) slices.\n * @type {number}\n */\n get workingGPUDensity () { return this.workingSliceCount * this.estimateGPUDensity; }\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 * When displayMaxDiagInfo is true, display stack trace and other enhanced diag info.\n * @type {boolean}\n **/\n get displayMaxDiagInfo ()\n {\n return common.displayMaxDiagInfo() || this.workerConsole && this.supervisor.options.allowConsoleAccess;\n }\n\n // _Idx\n //\n // addSlices, fetchDependencies, updateStatistics, update, predictLoad\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 {AuthMessage} authorizationMessage - The signature that shipped with the task authorizing this worker.\n * @returns {Promise<*>}\n */\n addSlices (sliceMessages, authorizationMessage)\n {\n /** @type {Array<Promise<void>>} */\n const slicePromises = [];\n sliceMessages.forEach((sliceMessage) => {\n const slice = new Slice(this, sliceMessage, authorizationMessage);\n if (!slice.isEstimation)\n this.isEstimation = false;\n this.sliceInventory.push(slice);\n slicePromises.push(slice.fetchSliceDataPromise);\n });\n return Promise_any(slicePromises);\n }\n\n /**\n * Fetch dependencies.\n * @param {SliceMessage[]} sliceMessages - Messages from task distributor describing slices.\n * @param {AuthMessage} authorizationMessage - The signature that shipped with the task authorizing this worker.\n * @returns {Promise<*>}\n */\n fetchDependencies (sliceMessages, authorizationMessage)\n {\n return this.fetchJob(this.jobMessage)\n .then (() => {\n debugging('supervisor') && console.debug('JobManager is transitioning to READY', this.identifier, Date.now() - this.supervisor.lastTime);\n this.state.set(INITIAL, READY);\n return this.addSlices(sliceMessages, authorizationMessage);\n }, (error) => {\n selectiveDebug() && console.error('fetchJob has failed', error);\n this.state.set(INITIAL, BROKEN);\n throw error;\n });\n }\n\n /** XXXpfr @todo Factor out statistics and EMA into another class. */\n /**\n * @param {number} x\n * @param {boolean} isDensity\n * @returns {boolean}\n */\n static isValid (x, isDensity = false)\n {\n return Number.isFinite(x) && !(x < 0) && !(isDensity && x > 1);\n }\n\n /**\n * Old legacy DCP schedulers can send a corrupt this.metrics .\n * We should get rid of this when we don't support old legacy DCP schedulers.\n */\n fixUpMetrics ()\n {\n if (!this.metrics)\n this.jobMessage.metrics = { sliceCPUTime: 0, sliceCPUDensity: 1, sliceGPUTime: 0, sliceGPUDensity: 0, sliceInDataSize: 0, sliceOutDataSize: 0 };\n else\n {\n if (!(JobManager.isValid(this.metrics.sliceCPUTime) && JobManager.isValid(this.metrics.sliceCPUDensity, true)))\n {\n this.metrics.sliceCPUTime = 1;\n this.metrics.sliceCPUDensity = 0.5;\n }\n if (!(JobManager.isValid(this.metrics.sliceGPUTime) && JobManager.isValid(this.metrics.sliceGPUDensity, true)))\n {\n this.metrics.sliceGPUTime = 1;\n this.metrics.sliceGPUDensity = 0.5;\n }\n if (!JobManager.isValid(this.metrics.sliceInDataSize))\n this.metrics.sliceInDataSize = 0;\n if (!JobManager.isValid(this.metrics.sliceOutDataSize))\n this.metrics.sliceOutDataSize = 0;\n }\n }\n\n /**\n * @param {Slice} slice\n * @param {Sandbox} sandbox\n * @returns {{CPUTime: number, GPUTime: number, CPUDensity: number, GPUDensity: number, InDataSize: number, OutDataSize: number}}\n */\n updateStatistics (slice, sandbox)\n {\n /**\n * @todo XXXpfr Make this only check when debugBuild, but not until the Sup2 churn has stopped.\n * @param {{CPUTime: number, GPUTime: number, CPUDensity: number, GPUDensity: number, InDataSize: number, OutDataSize: number}} metrics\n * @returns {boolean}\n */\n const isValidMetrics = (metrics) => {\n return JobManager.isValid(metrics.CPUTime) && JobManager.isValid(metrics.CPUDensity, true)\n && JobManager.isValid(metrics.GPUTime) && JobManager.isValid(metrics.GPUDensity, true)\n && JobManager.isValid(metrics.InDataSize) && JobManager.isValid(metrics.OutDataSize);\n }\n\n const timeReport = slice.timeReport;\n const dataReport = slice.dataReport;\n selectiveDebug() && console.debug('updateStatistics', slice.identifier, timeReport, dataReport);\n\n // Sanity\n if (!(timeReport.CPU >= 1))\n {\n timeReport.CPU = 1;\n timeReport.total += 1;\n }\n if (!(timeReport.total >= 1))\n timeReport.total = 1;\n\n // Construct metrics.\n const GPUTime = timeReport.webGPU + timeReport.webGL;\n if (this.useGPU)\n updateGPUStatistics(GPUTime);\n const metrics = {\n CPUTime: timeReport.CPU,\n GPUTime,\n CPUDensity: timeReport.CPU / timeReport.total,\n GPUDensity: GPUTime / timeReport.total,\n InDataSize: dataReport.InDataSize,\n OutDataSize: dataReport.OutDataSize,\n };\n //console.debug('JM.updateStatistics metrics:', metrics, timeReport.total);\n\n // Create the measurement object which will be passed to the metrics event.\n const eventMeasurements = {\n elapsed: timeReport.total / 1000,\n CPU: metrics.CPUTime / 1000,\n GPU: metrics.GPUTime / 1000,\n in: metrics.InDataSize,\n out: metrics.OutDataSize,\n };\n\n // Emit metrics event for both jobHandle and sandboxHandle\n this.jobEmit( 'metrics', slice.sliceNumber, eventMeasurements);\n this.sandboxEmit(sandbox, 'metrics', slice.sliceNumber, eventMeasurements);\n selectiveDebug() && console.debug(`updateStatistics: slice ${slice.identifier}, eventMeasurements ${stringify(eventMeasurements)}`);\n\n if (!isValidMetrics(metrics))\n throw new Error(`JobManager.updateStatistics: metrics are in an inconsistent state ${JSON.stringify(metrics)}`);\n\n if (!this.localMetrics)\n this.localMetrics = { CPUTime: 0, GPUTime: 0, TotalTime: 0, CPUDensity: 0, GPUDensity: 0, InDataSize: 0, OutDataSize: 0 };\n\n // measuredSlices+1 because the current slice hasn't been measured yet in RS.updateMetrics.\n const alpha = this.metrics?.lastSliceNumber\n ? constructAlpha(this.metrics.lastSliceNumber, this.metrics.measuredSlices + 1)\n : 0.5;\n selectiveDebug2() && console.debug(`updateStatistics: local alpha ${alpha}, global alpha ${this.metrics?.alpha}`);\n this.localMetrics.CPUTime = nextEma(this.localMetrics.CPUTime, metrics.CPUTime, alpha);\n this.localMetrics.GPUTime = nextEma(this.localMetrics.GPUTime, metrics.GPUTime, alpha);\n this.localMetrics.TotalTime = nextEma(this.localMetrics.TotalTime, timeReport.total, alpha);\n this.localMetrics.CPUDensity = nextEma(this.localMetrics.CPUDensity, metrics.CPUDensity, alpha);\n this.localMetrics.GPUDensity = nextEma(this.localMetrics.GPUDensity, metrics.GPUDensity, alpha);\n this.localMetrics.InDataSize = nextEma(this.localMetrics.InDataSize, metrics.InDataSize, alpha); // Don't need this yet.\n this.localMetrics.OutDataSize = nextEma(this.localMetrics.OutDataSize, metrics.OutDataSize, alpha); // Don't need this yet.\n\n if (common.debugQuanta())\n {\n this.supervisor.dbg.addRawLocal(slice, metrics, alpha);\n this.supervisor.dbg.addLocal(slice, this.emaSliceTime, alpha);\n }\n\n if (!isValidMetrics(this.localMetrics))\n throw new Error(`JobManager.updateStatistics: localMetrics are in an inconsistent state ${JSON.stringify(this.localMetrics)}`);\n\n debugging('supervisor') && console.debug('updateStatistics: ema', this.statistics.ema, 'emaSliceTime', this.emaSliceTime, 'estimateDensity', this.estimateDensity, 'metrics', metrics, 'g-metrics', this.metrics, 'alpha', alpha);\n //if (!Number.isFinite(this.statistics.ma))\n // console.log(this.statistics.x);\n return metrics;\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 getJobsForTask.\n * @param {SliceMessage[]} [sliceMessages] - Messages from task distributor describing slices.\n * @param {AuthMessage} [authorizationMessage] - The signature that shipped with the task authorizing this worker.\n * @returns {Promise<*>}\n */\n update (jobMessage, sliceMessages, authorizationMessage)\n {\n Object.assign(this.jobMessage, jobMessage);\n /*\n * Make sure this.metrics contains valid data.\n * With old legacy schedulers, this may not be so.\n */\n this.fixUpMetrics();\n selectiveDebug2() && console.debug('JM.update: new this.jobMessage', this.jobMessage.metrics);\n\n if (sliceMessages)\n return (this.#jobPromise = this.addSlices(sliceMessages, authorizationMessage));\n }\n\n /**\n * Predict the total reduction in density of working sandboxes timeSpanMs from now.\n * This function is called right before fetchTask, in order to calculate how much space is available.\n * In pratice, the returned property queued will have length <= 1, because when it is more we won't call fetchWork.\n * @param {number} timeSpanMs\n * @returns {{ queued: Slice[], working: number }}\n */\n predictLoad (timeSpanMs)\n {\n let queuedCount = this.queuedSlices.length;\n // Optimize to short-circuit when queued > 1, because we won't call fetchWork in that case.\n if (queuedCount > this.workingSliceCount + 1)\n {\n selectiveDebug() && console.debug('JM.predictLoad: bail early', queuedCount, this.workingSliceCount);\n return { queued: [null, null], working: this.workingSliceDensity };\n }\n let space = 0;\n // 1) Predict the current working slices that will finish in timeSpanMs.\n // 2) Compensate for queued slices that may begin working.\n for (const slice of this.sliceInventory)\n {\n if (!slice.isReserved && !slice.isWorking)\n continue;\n const remainingTime = Math.max(0, this.emaSliceTime - (Date.now() - slice.startTime));\n selectiveDebug2() && console.debug('JM.predictLoad: predicting...', queuedCount, space, this.emaSliceTime, remainingTime, this.emaSliceTime <= timeSpanMs - remainingTime);\n // If slice will end before timeSpanMs elapses, it may open up space.\n if (remainingTime <= timeSpanMs)\n {\n // Check to see whether a new queued slices can start running and finish in the remaining time.\n // Somewhat inaccurate because it's possible multiple slices could finish in the remaining time.\n // e.g. Suppose timeSpanMs = 5 * 1000 and this.emaSliceTime is 1000, then there's room\n // to successively schedule 5 slices, one after another.\n /** XXXpfr @todo Fix to incorporate successive scheduling. */\n if (queuedCount > 0)\n {\n --queuedCount;\n // If the new slice finishes in time, count it as opening up space.\n if (this.emaSliceTime <= timeSpanMs - remainingTime)\n ++space;\n }\n else // No queued slice can run so space has opened up.\n ++space;\n }\n }\n // Optimize to short-circuit when queued > 1, because we won't call fetchWork in that case.\n let queued = [null, null];\n if (queuedCount < 2)\n queued = (queuedCount === 1) ? [this.queuedSlices[0]] : [];\n return { queued, working: (this.workingSliceCount - space) * this.estimateDensity };\n }\n\n // _Idx\n //\n // reserveOneSlice, resetSlices, destroy, evaluatorDownCleanup\n //\n\n /**\n * Find one ready slice, mark it as working and return.\n * If there are no ready slices return undefined.\n * @returns {Slice}\n */\n reserveOneSlice ()\n {\n for (const slice of this.sliceInventory)\n if (slice.isReady)\n return slice.markAsReserved();\n return null;\n }\n\n /**\n * Error recovery of slices.\n * When evaluator is down and it goes up again, there may be\n * working slices from working sandboxes that were terminated.\n * @param {string} [tag]\n */\n resetSlices (tag)\n {\n this.sliceInventory.forEach((slice) => {\n if (this.evaluatorDownHandle)\n this.evaluatorDownHandle.intr(); // Interrupt a$sleep in evaluatorDownCleanup\n this.evaluatorDownHandle = null;\n if (slice.isWorking || slice.isFailed)\n slice.resetState();\n });\n //this.dumpSlices(tag); // SAVE\n }\n\n /**\n * Destructor.\n * @returns {Promise<*>}\n */\n destroy ()\n {\n selectiveDebug() && console.debug(`JobManager.destroy: terminating sandboxes and returning slices to scheduler for job manager ${this.identifier}.`);\n this.sandboxInventory.forEach((sandbox) => { this.supervisor.returnSandbox(sandbox); });\n const reason = 'JobManager destroy';\n return this.supervisor.returnSlices(this.unFinishedSlices, reason)\n .finally (() => {\n this.#sliceInventory = [];\n this.state.removeAllListeners();\n if (this.state.isNot(BROKEN))\n this.state.set([ INITIAL, READY, STOP, REFUSE ], BROKEN);\n this.jobEmit('flush');\n });\n }\n\n /**\n * Destructor.\n * Called when the evaluator goes down (or screensaver goes up), in the sandbox 'terminated' event handler in Supervisor.\n * A slice marked as COMPLETE but not FINISHED means it still needs to submit results.\n * This function allows such slices to still submit results.\n * Because sometimes a screensaver only goes down for a minute or less (e.g. the user needs to look at something quick),\n * we delay an average of 90 (or 1.5 * delay) seconds before returning non-complete slices.\n * param {number} [delay=60] - Average delay is 1.5 * delay seconds.\n */\n evaluatorDownCleanup (delay = 60)\n {\n selectiveDebug() && console.debug(`JobManager.evaluatorDownCleanup: delay returning non-complete slices for on average ${1.5 * delay} seconds, ${this.identifier}.`);\n if (this.sandboxInventory.length > 0)\n this.warning('JobManager.evaluatorDownCleanup: When the evaluator is down there should not be any non-terminated sandboxes left.');\n // Call retVal.intr() to interrupt and cancel the timer inside a$sleepMs.\n return (this.evaluatorDownHandle = a$sleepMs(delay * (1.0 + Math.random()) * 1000)\n .finally(() => {\n const slicesToReturn = this.sliceInventory.filter((slice) => !(slice.isComplete || slice.isFinished));\n const reason = 'JobManager evaluatorDownCleanup';\n return this.supervisor.returnSlices(slicesToReturn, reason);\n }));\n }\n\n // _Idx\n //\n // assignSandbox, runSlice, runSliceOnSandbox\n // Functions chain and return non-awaited promises.\n //\n\n /**\n * Create a Sandbox.\n * Start it. Assign it. Add it to jobManager inventory.\n * @param {Slice} slice\n * @returns {Promise<Sandbox>}\n */\n async assignSandbox (slice)\n {\n try\n {\n // We want to await createSandbox so we don't create a large number of sandboxes all at once.\n const sandbox = await this.supervisor.createSandbox()\n .catch((error) => {\n // Catches exception from createSandbox.\n // Shound be rare unless evaluator or screensaver goes down.\n selectiveDebug2() && !this.supervisor.evaluator.down && console.debug('assignSandbox: createSandbox error', slice.identifier, error);\n slice.unReserve(); // reuse slice\n throw error;\n });\n selectiveDebug2() && console.debug('assignSandbox', slice.identifier);\n sandbox.slice = slice; // Must be before sandbox.assign; cf. fetchModule exception in handleRing2Message.\n return sandbox.assign(this)\n .then((assignedSandbox) => {\n selectiveDebug2() && console.debug('assignSandbox: success', sandbox.identifier);\n this.supervisor.sandboxInventory.push(assignedSandbox);\n return assignedSandbox;\n })\n .catch((error) => {\n // Exception is from sandbox.assign.\n // It could be a string coming from evaluator.\n this.error(`Failed to assign job to sandbox ${sandbox.identifier}`, error);\n slice.unReserve(); // reuse slice\n throw error;\n });\n }\n catch (error)\n {\n this.error(`JobManager.assignSandbox failed to find sandbox to run slice ${slice.identifier}`, error);\n throw error;\n }\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 * @returns {Promise<any>}\n */\n runSlice (slice)\n {\n try\n {\n selectiveDebug() && console.debug('runSlice:', slice.identifier, 'assigned/working', this.assignedSandboxes.length, this.workingSandboxes.length);\n\n if (this.supervisor.sliceTiming)\n slice['queueingDelta'] = Date.now();\n\n const _assignedSandboxes = this.assignedSandboxes;\n if (_assignedSandboxes.length > 0)\n {\n const sandbox = _assignedSandboxes[0];\n selectiveDebug2() && console.debug(`runSlice(assigned): sandbox ${sandbox.identifier}`, Date.now() - this.supervisor.lastTime);\n // When evaluator goes down, all sandboxes are terminated.\n // A ready slice has a finite lifetime and if exceeded may transition back to READY, in which\n // case we bail since it might already be in the process of being selected in roundRobinSlices.\n if (!slice.markAsWorking() || sandbox.isTerminated)\n return Promise.resolve();\n sandbox.markAsWorking();\n sandbox.slice = slice; // Reusing assigned sandbox, so set the slice.\n return this.runSliceOnSandbox(slice, sandbox)\n .catch((error) => {\n // Catches exception from runSliceOnSandbox; s.b. rare.\n this.error(`JobManager.runSlice: Failure trying to run slice ${slice.identifier} on a assigned sandbox.`, error);\n return this.supervisor.returnSlice(slice, 'JobManager.runSlice');\n });\n }\n\n return this.assignSandbox(slice)\n .then((sandbox) => {\n selectiveDebug2() && console.debug(`runSlice: sandbox ${sandbox.identifier}`, Date.now() - this.supervisor.lastTime);\n // When evaluator goes down, all sandboxes are terminated.\n // A ready slice has a finite lifetime and if exceeded may transition back to READY, in which\n // case we bail since it might already be in the process of being selected in roundRobinSlices.\n if (!slice.markAsWorking() || sandbox.isTerminated)\n return Promise.resolve();\n sandbox.markAsWorking();\n return this.runSliceOnSandbox(slice, sandbox);\n }, (error) => {\n // Catches exception from assignSandbox for error control flow.\n selectiveDebug() && console.error(`JobManager.runSlice: Failure from assignSandbox when trying to run slice ${slice.identifier} on a sandbox.`, error);\n })\n .catch((error) => {\n // Catches exception from runSliceOnSandbox; s.b. rare.\n this.error(`JobManager.runSlice: Failure trying to run slice ${slice.identifier} on a sandbox.`, error);\n return this.supervisor.returnSlice(slice, 'JobManager.runSlice');\n });\n }\n catch (error)\n {\n this.error(`JobManager.runSlice failed to run slice ${slice.identifier}`, error);\n return this.supervisor.returnSlice(slice, 'JobManager.runSlice_static_catch');\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 * @returns {Promise<any>}\n */\n runSliceOnSandbox (slice, sandbox)\n {\n const handleError = (error, tag) => {\n if (sandbox.isTerminated)\n {\n this.error(`The screensaver or evaluator may be down: slice ${slice.identifier}`, error);\n return this.supervisor.returnSlice(slice, `${tag}: sandbox terminated`);\n }\n else\n {\n this.error(`Failure in ${tag} for slice ${slice.identifier}`, error);\n const reason = (error.code === 'EPERM_ORIGIN' || error.code === 'EFETCH_BAD_ORIGIN' || error.code === 'EFETCH') ? error.code : tag;\n this.supervisor.returnSlice(slice, reason);\n }\n };\n debugging('supervisor') && console.debug(`runSliceOnSandbox ${sandbox.identifier} #(r/rsv/w/wsbx/sbx), ${this.supervisor.dbg.workingSliceSandboxStr}, #assigned, ${this.assignedSandboxes.length}, #localSBs, ${this.sandboxInventory.map(s => Number(s.id)).sort((x,y)=>x-y)}`);\n selectiveDebug() && console.debug('runSliceOnSandbox', sandbox.identifier, this.workingSliceCount, Date.now() - this.supervisor.lastTime);\n\n if (sandbox.isTerminated) // When evaluator goes down, all sandboxes are terminated.\n return Promise.resolve();\n\n this.sandboxEmit(sandbox, 'slice', slice.sliceNumber);\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 return sandbox.work()\n .finally(() => {\n if (this.supervisor.sliceTiming)\n {\n slice['executionDelta'] = Date.now() - slice['executionDelta'];\n slice['resultDelta'] = Date.now();\n }\n })\n .then((result) => {\n selectiveDebug2() && console.debug(`runSliceOnSandbox: sandbox${sandbox.id} - success: ${slice.identifier}`, Boolean(result), Date.now() - slice.startTime);\n\n if (sandbox.isTerminated)\n throw new Error(`Slice ${slice.identifier} completed work, but the sandbox ${sandbox.id} is terminated.`);\n\n // Allow reuse of sandbox up to a limit of dcpConfig.supervisor.sandbox.maxSandboxUse .\n sandbox.checkSandboxReUse();\n // Transition slice to COMPLETE\n slice.collectResult(result, true /*success*/);\n\n // Record result.\n return this.supervisor.recordResult(slice, sandbox)\n .catch((error) => {\n // Catches exception from supervisor.recordResult .\n return handleError(error, 'recordResult');\n });\n\n }, (error) => {\n // Catches exception from sandbox.work.\n // Kills sandbox and either retries the slice or transitions slice to FAILED and returns to scheduler.\n const reason = this.supervisor.handleSandboxWorkError(sandbox, slice, error);\n selectiveDebug() && console.error(`runSliceOnSandbox: Failure executing slice on sandbox: ${sandbox.identifier}`, reason, error);\n })\n .catch((error) => {\n // Catches exception from sandbox.work or then-clause.\n return handleError(error, 'runSliceOnSandbox');\n });\n }\n\n // _Idx\n //\n // removeSlice\n //\n\n /**\n * Remove slice from this.sliceInventory.\n * @param {Slice} slice - The slice to remove from this.sliceInventory.\n * @return {boolean} - False when slice is not in this.sliceInventory.\n */\n removeSlice (slice)\n {\n debugging('supervisor') && console.debug(`removeSlice slice ${slice.sliceNumber}`);\n return common.removeElement(this.sliceInventory, slice);\n }\n\n // _Idx\n //\n // fetchError, fetchJob, fetchSliceData\n //\n\n fetchError (error)\n {\n if (error.code === 'EPERM_ORIGIN')\n throw new BadOriginError(error);\n if (error.code === 'EFETCH')\n throw new RemoteFetchError(error);\n throw new UncaughtExceptionError(error);\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 * @returns {Promise<*>}\n */\n fetchJob (mpe)\n {\n debugging('supervisor') && utils.dumpObject(mpe, 'JobManager.fetchJob: mpe', 512);\n\n const promises = [];\n try\n {\n // Get workFn.\n if (!mpe.workFunction)\n {\n const workFunctionPromise = this.fetchWorkFunctions(mpe.codeLocation)\n .then((workFunction) => {\n this.inputDataSize += workFunction.size;\n mpe.workFunction = workFunction.data;\n if (mpe.requirements.useStrict)\n mpe.useStrict = true;\n delete mpe.codeLocation;\n }, (error) => {\n this.fetchError(error);\n });\n promises.push(workFunctionPromise);\n }\n\n if (!mpe.mro && mpe.MROLocation)\n {\n const mroPromise = this.fetchData(mpe.MROLocation)\n .then((mro) => {\n // It's not ready for computing the inputDataSize yet -- that's done in JobManager.fetchSliceData .\n mpe.mro = mro.data;\n delete mpe.MROLocation;\n }, (error) => {\n this.fetchError(error);\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(this.fetchArguments(mpe.argumentsLocation[k].value)\n .then((arg) => {\n this.inputDataSize += arg.size;\n mpe.arguments[k] = arg.data;\n }, (error) => {\n this.fetchError(error);\n })\n );\n }\n\n return Promise.all(promises)\n .then(() => {\n if (mpe.argumentsLocation)\n delete mpe.argumentsLocation;\n return mpe;\n }, (error) => {\n this.fetchError(error);\n });\n }\n catch (error)\n {\n this.fetchError(error);\n }\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<{ data: *, size: number }>}\n */\n fetchSliceData (datumUri, slice)\n {\n try\n {\n if (!datumUri)\n {\n if (!this.mro)\n throw new Error('Must complete call to JobManager.fetchJob before calling JobManager.fetchSliceData.');\n /** XXXpfr @todo Inefficient, we're rehydrating the whole range for a single slice datum. */\n const ro = rehydrateRange(this.mro);\n // Slice numbers start at 1.\n const sliceDatum = ro[slice.sliceNumber - 1];\n debugging('supervisor') && console.debug(`Fetched mro datum: ${stringify(sliceDatum, 512)}`);\n return Promise.resolve({ data: sliceDatum, size: JSON.stringify(sliceDatum).length });\n }\n\n return this.fetchData(datumUri)\n .catch((error) => {\n throw this.fetchError(error);\n });\n }\n catch (error)\n {\n this.fetchError(error);\n }\n }\n\n // _Idx\n //\n // jobEmit, sandboxEmit, error, warning. countSliceStr, dumpSlices\n //\n\n /**\n * Safe event emitter on jobHandle.\n * @param {string} event\n * @param {...any} args\n */\n jobEmit(event, ...args)\n {\n this.supervisor.safeEmit(this.jobHandle, event, ...args);\n }\n\n /**\n * Safe event emitter on sandboxHandle.\n * @param {Sandbox} sandbox\n * @param {string} event\n * @param {...any} args\n */\n sandboxEmit(sandbox, event, ...args)\n {\n this.supervisor.safeEmit(sandbox.sandboxHandle, event, ...args);\n }\n\n /**\n * Error feedback to user.\n * @param {string} message\n * @param {Array<Error>|Error|string} [coreError]\n * @param {string} [additionalInfo]\n */\n error (message, coreError, additionalInfo)\n {\n this.supervisor.error(message, coreError, additionalInfo);\n }\n\n /**\n * Warning feedback to user.\n * @param {string[]} messages\n */\n warning (...messages)\n {\n this.supervisor.warning(...messages);\n }\n\n /**\n * Debugging helper.\n * @param {string} [tag='']\n * @returns {string}\n */\n countSliceStr (tag)\n {\n const { unassigned, ready, reserved, working, workdone, complete, failed, finished } = this.dumpSlices (tag, false, false);\n return `${tag}: u/r/rsv/w/wd/c/f/fsh ${unassigned}/${ready}/${reserved}/${working}/${workdone}/${complete}/${failed}/${finished}`;\n }\n\n /**\n * @param {URL|string} uri\n * @returns {Promise<*>}\n */\n fetchData (uri) { return fetchURIAndLength(uri, this.originManager, allowOriginsPurposes.fetchData); }\n /**\n * @param {URL|string} uri\n * @returns {Promise<*>}\n */\n fetchArguments (uri) { return fetchURIAndLength(uri, this.originManager, allowOriginsPurposes.fetchArguments); }\n /**\n * @param {URL|string} uri\n * @returns {Promise<*>}\n */\n fetchWorkFunctions (uri) { return fetchURIAndLength(uri, this.originManager, allowOriginsPurposes.fetchWorkFunctions); }\n\n /**\n * Debugging helper.\n * @param {string} [tag='']\n * @param {boolean} [details=false]\n * @param {boolean} [display=true]\n * @returns {{ ready, reserved, working, workdone, complete, failed, finished, unassigned }}\n */\n dumpSlices (tag = '', details = false, display = true)\n {\n if (display)\n console.log(`dumpSlices(${tag}): ${this.identifier}, sliceCount ${this.sliceInventory.length}, sandboxCount ${this.sandboxInventory.length}`, Date.now() - this.supervisor.lastTime);\n if (this.sliceInventory.length < 1) return { unassigned:0, ready:0, reserved:0, working:0, workdone:0, complete:0, failed:0, finished:0 };\n if (details)\n {\n // Detailed classification of slices.\n const unassigned = [], ready = [], reserved = [], working = [], workdone = [], complete = [], failed = [], finished = [];\n for (const slice of this.sliceInventory)\n {\n if (slice.isReady)\n ready.push(slice);\n else if (slice.isReserved)\n reserved.push(slice);\n else if (slice.isWorking)\n working.push(slice);\n else if (slice.isWorkDone)\n workdone.push(slice);\n else if (slice.isComplete)\n complete.push(slice);\n else if (slice.isFailed)\n failed.push(slice);\n else if (slice.isFinished)\n finished.push(slice);\n else if (slice.isUnassigned)\n unassigned.push(slice);\n else\n throw new Error(`dumpSlices: Unexpected kind a slice; ${slice.identifier}`);\n }\n if (display)\n {\n const dmpSlices = (name, slices) => {\n console.log(`-----${name}(${slices.length})-----------------------------------------------------------------`);\n for (const slice of slices)\n console.log(slice.identifier);\n }\n dmpSlices('unassigned', unassigned);\n dmpSlices('ready', ready);\n dmpSlices('reserved', reserved);\n dmpSlices('working', working);\n dmpSlices('workdone', workdone);\n dmpSlices('complete', complete);\n dmpSlices('failed', failed);\n dmpSlices('finished', finished);\n console.log('-----------------------------------------------------------------------------------');\n }\n return {\n ready: ready.length,\n reserved: reserved.length,\n working: working.length,\n workdone: workdone.length,\n complete: complete.length,\n failed: failed.length,\n finished: finished.length,\n unassigned: unassigned.length,\n };\n }\n else\n {\n // Quick classification of slices.\n const { ready,\n reserved,\n working,\n workdone,\n complete,\n failed,\n finished,\n unassigned } = common.sliceCounts(this.sliceInventory);\n if (display)\n {\n const dumpCount = (name, count) => {\n console.log(`-----${name}(${count})-----------------------------------------------------------------`);\n }\n dumpCount('unassigned', unassigned);\n dumpCount('ready', ready);\n dumpCount('reserved', reserved);\n dumpCount('working', working);\n dumpCount('workdone', workdone);\n dumpCount('complete', complete);\n dumpCount('failed', failed);\n dumpCount('finished', finished);\n console.log('-----------------------------------------------------------------------------------');\n }\n return { ready, reserved, working, workdone, complete, failed, finished, unassigned };\n }\n }\n\n}\nexports.JobManager = JobManager;\n\n\n//# sourceURL=webpack://dcp/./src/dcp-client/worker/supervisor2/job-manager.js?");
|
|
4738
|
+
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@distributive.network,\n * Paul, paul@distributive.network,\n * @date Dec 2020,\n * June 2022, Jan-April 2023\n * @module job-manager\n * @copyright Copyright (c) 2018-2023, Distributive Corp. All Rights Reserved\n */\n// @ts-check\n\n\n/** @typedef {string} opaqueId */ // 22 character base64 string\n/** @typedef {string} address */ // String(Address)\n/** @typedef {import('./sandbox2').Sandbox} Sandbox */\n/** @typedef {import('./index').Supervisor} Supervisor */\n/** @typedef {import('dcp/utils/jsdoc-types').SliceMessage} SliceMessage */\n/** @typedef {import('dcp/utils/jsdoc-types').Auth} Auth */\n/** @typedef {import('dcp/utils/jsdoc-types').Signature} Signature */\n/** @typedef {import('dcp/utils/jsdoc-types').AuthMessage} AuthMessage */\n/** @typedef {import('dcp/dcp-client/worker/origin-access-manager').OriginAccessManager} OriginAccessManager */\n\nconst inspect = Symbol.for('nodejs.util.inspect.custom');\nconst debugging = (__webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope)('worker');\nconst EventEmitter = __webpack_require__(/*! events */ \"./node_modules/events/events.js\");\nconst { Inventory } = __webpack_require__(/*! dcp/utils/inventory */ \"./src/utils/inventory.js\");\nconst { Synchronizer } = __webpack_require__(/*! dcp/common/concurrency */ \"./src/common/concurrency.js\");\nconst { rehydrateRange } = __webpack_require__(/*! dcp/dcp-client/range-object */ \"./src/dcp-client/range-object.js\");\nconst { fetchURIAndLength } = __webpack_require__(/*! dcp/utils/fetch-uri */ \"./src/utils/fetch-uri.js\");\nconst utils = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\nconst { stringify, a$sleepMs, constructAlpha, nextEma } = utils;\nconst { allowOriginsPurposes } = __webpack_require__(/*! dcp/common/worker-constants */ \"./src/common/worker-constants.js\");\n\nconst { Slice } = __webpack_require__(/*! ./slice2 */ \"./src/dcp-client/worker/supervisor2/slice2.js\");\nconst { Promise_any } = __webpack_require__(/*! ./promise_any */ \"./src/dcp-client/worker/supervisor2/promise_any.js\");\nconst { Statistics } = __webpack_require__(/*! ./rolling-statistics */ \"./src/dcp-client/worker/supervisor2/rolling-statistics.js\");\nconst { SandboxError, UncaughtExceptionError } = __webpack_require__(/*! ./sandbox2 */ \"./src/dcp-client/worker/supervisor2/sandbox2.js\");\nconst { updateGPUStatistics } = __webpack_require__(/*! ./gpu_support */ \"./src/dcp-client/worker/supervisor2/gpu_support.js\");\nconst common = __webpack_require__(/*! ./common */ \"./src/dcp-client/worker/supervisor2/common.js\");\nconst { selectiveDebug, selectiveDebug2, debugBuild } = common;\n\nconst INITIAL = 'initial';\nconst READY = 'ready';\nconst STOP = 'stop';\nconst REFUSE = 'refuse';\nconst BROKEN = 'broken';\n\nclass BadOriginError extends SandboxError { constructor(msg) { super('EPERM_ORIGIN', msg); } }\nclass RemoteFetchError extends SandboxError { constructor(msg) { super('EFETCH', msg); } }\n\n/**\n * Public event emitter.\n * https://gitlab.com/Distributed-Compute-Protocol/dcp-docs-wes/-/blob/wip/worker/worker-events.md\n */\nclass JobHandle extends EventEmitter\n{\n /** @type {JobManager} */\n #jobManager;\n \n /**\n * @constructor\n * @param {JobManager} jobManager\n */\n constructor (jobManager)\n {\n super({ captureRejections: false });\n this.#jobManager = jobManager;\n }\n /** @type {string} */\n get address () { return this.#jobManager.address; }\n /** @type {string} */\n get name () { return this.#jobManager.public ? this.#jobManager.public.name : '<unknown>'; }\n /** @type {string} */\n get description () { return this.#jobManager.public ? this.#jobManager.public.description : '<unknown>'; }\n /** @type {string} */\n get link () { return this.#jobManager.public ? this.#jobManager.public.link : '<unknown>'; }\n}\nexports.JobHandle = JobHandle;\n\n//\n// Index to functionality -- search for '_Idx' to toggle through the index.\n//\n// 1) class JobManager\n// 2) addSlices, fetchDependencies, updateStatistics, update, predictLoad\n// 3) reserveOneSlice, resetSlices, destroy, evaluatorDownCleanup\n// 4) assignSandbox, runSlice, runSliceOnSandbox\n// 5) removeSlice\n// 6) fetchError, fetchJob, fetchSliceData\n// 7) jobEmit, sandboxEmit, error, warning. countSliceStr, dumpSlices.\n//\n\n// _Idx\n//\n// class JobManager\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 * initial -> 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\n{\n /** @type {Supervisor} */\n #supervisor;\n /** @type {Slice[]} */\n #sliceInventory;\n /** @type {Synchronizer} */\n #state;\n /** @type {*} */ // Figure out the type\n #jobMessage;\n /** @type {string} */\n #address;\n /** @type {Statistics} */\n #statistics;\n /** @type {Promise<*>} */\n #jobPromise;\n\n /**\n * @constructor\n * @param {Supervisor} parent - Owning Supervisor.\n * @param {object} jobMessage - Job Descriptor from getJobsForTask.\n * @param {SliceMessage[]} sliceMessages - Messages from task distributor describing slices.\n * @param {AuthMessage} authorizationMessage - The signature that shipped with the task authorizing this worker.\n */\n constructor(parent, jobMessage, sliceMessages, authorizationMessage)\n {\n this.#supervisor = parent;\n this.#sliceInventory = []; // All slices for this.address.\n this.#state = new Synchronizer(INITIAL, [ INITIAL, READY, STOP, REFUSE, BROKEN ]);\n this.#jobMessage = { ...jobMessage };\n this.#address = String(this.jobMessage.address);\n this.#statistics = new Statistics(0.5); // Effective period of 12 or 13\n this.#jobPromise = null;\n\n /** @type {{CPUTime: number, GPUTime: number, TotalTime: number, CPUDensity: number, GPUDensity: number, InDataSize: number, OutDataSize: number }} */\n this.localMetrics = null;\n /** @type {string[]} */\n this.rejectedJobReasons = [];\n /** @type {boolean} */\n this.isEstimation = true;\n /** @type {number} */\n this.inputDataSize = 0;\n /**\n * jobMan.evaluatorDownCleaup returns this.evaluatorDownHandle=a$sleepMs which is\n * interruptible by calling this.evaluatorDownHandle.intr().\n * @type {Promise<void>}\n */\n this.evaluatorDownHandle = null;\n\n /*\n * Make sure this.metrics contains valid data.\n * With old legacy schedulers, this may not be so.\n */\n this.fixUpMetrics();\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 for work. Do not place this job manager\n * in inventory until 'ready'.\n */\n this.#jobPromise = this.fetchDependencies(sliceMessages, authorizationMessage)\n .then(() => {\n this.supervisor.jobManagerInventory.push(this);\n });\n\n /* Check for GPU support. */\n const _env = this.requirements?.environment;\n /** @type {boolean} */\n this.useGPU = _env ? (_env.webgpu || _env.offscreenCanvas) : false;\n \n /**\n * Event emitter containing info that describes the job.\n * @type {JobHandle}\n */\n this.jobHandle = new JobHandle(this);\n\n /**\n * Emit the 'job' event on the worker event emitter.\n */\n this.supervisor.safeEmit(this.supervisor.worker, 'job', this.jobHandle);\n }\n\n /** @type {*} */ // Figure out the type\n get jobMessage () { return this.#jobMessage; }\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 /** @type {boolean} */\n get almostDone () { return this.jobMessage.almostDone; }\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, sliceInDataSize: number, sliceOutDataSize: number, lastSliceNumber: number, measuredSlices: number, alpha: 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 ready () { return this.state.is(READY); }\n\n // These 4 aren't needed yet.\n ///** @type {boolean} */\n //get initial () { return this.state.is(INITIAL); }\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.supervisor.sandboxInventory.filter((sandbox) => sandbox.jobAddress === this.#address && !sandbox.isTerminated); }\n /** @type {Sandbox[]} */\n get assignedSandboxes () { return this.supervisor.sandboxInventory.filter((sandbox) => sandbox.jobAddress === this.#address && sandbox.isAssigned); }\n /** @type {Sandbox[]} */\n get workingSandboxes () { return this.supervisor.sandboxInventory.filter((sandbox) => sandbox.jobAddress === this.#address && sandbox.isWorking); }\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[]} */\n get queuedSlices () { return this.sliceInventory.filter((slice) => slice.isQueued); } // Ready and soon-to-be-ready\n /** @type {Slice[]} */\n get reservedSlices () { return this.sliceInventory.filter((slice) => slice.isReserved); }\n /** @type {Slice[]} */\n get activeSlices () { return this.sliceInventory.filter((slice) => slice.isActive); } // Reserved, working, workdone and complete\n /** @type {Slice[]} */\n get workingSlices () { return this.sliceInventory.filter((slice) => slice.isWorking || slice.isReserved); } // Working and soon-to-be-working\n /** @type {Slice[]} */\n get unFinishedSlices () { return this.sliceInventory.filter((slice) => !slice.isFinished); }\n\n /** @type {Statistics} */\n get statistics () { return this.#statistics; }\n /** @type {Promise<*>} */\n get jobPromise () { return this.#jobPromise; }\n /** @type {number} */\n get globalTime () { return utils.globalTime(this.metrics); }\n /** @type {number} */\n get emaSliceTime () { return this.localMetrics ? utils.localTime(this.localMetrics) : (utils.globalTime(this.metrics) * this.supervisor.defaultQuanta); }\n /** @type {number} */\n get emaTotalTime () { return this.localMetrics?.TotalTime || (utils.globalTotalTime(this.metrics) * this.supervisor.defaultQuanta); }\n /** @type {number} */\n get adjSliceTime () { return Math.min(this.emaSliceTime, this.supervisor.options.targetTaskDuration); }\n /** @type {string} */\n get identifier () { return `${common.truncateAddress(this.address)}.${this.state}`; }\n /** @type {string} */\n get sliceState () { return `${this.queuedSlices.length}.${this.workingSliceCount}.${this.sliceInventory.length}.${this.workRemaining}`; }\n /** @type {string} */\n get [inspect] () { return `[Object JobManager <${this.public.name}::${common.truncateAddress(this.address)}::${this.state}>]`; }\n /** @type {OriginAccessManager} */\n get originManager() { return this.supervisor.originManager; }\n /**\n * Expected density of a slice.\n * When we get the GPU stuff codified, the definition might be\n * { cpu: this.localMetrics?.CPUDensity ?? 1.0, gpu: this.localMetrics?.GPUDensity ?? 1.0}\n * @type {number}\n */\n get estimateDensity () { return this.localMetrics?.CPUDensity || utils.globalDensity(this.metrics); }\n /** @type {number} */\n get estimateGPUDensity () { return this.localMetrics?.GPUDensity || utils.globalGPUDensity(this.metrics); }\n /** @type {number} */\n get cpuTime () { return this.localMetrics?.CPUTime || this.metrics?.sliceCPUTime || 0; }\n /**\n * Estimate the number of milliseconds of total execution time of the job.\n * @type {number}\n */\n get workRemaining ()\n {\n const _adjSliceTime = this.adjSliceTime;\n const queuedTime = _adjSliceTime * this.queuedSlices.length;\n let workingTime = 0;\n for (const slice of this.workingSlices)\n {\n workingTime += Math.max(0, _adjSliceTime - (Date.now() - slice.startTime));\n selectiveDebug2() && console.debug(`JobManager.workRemaining: _adjSliceTime ${_adjSliceTime}, slice-time-elapsed ${Date.now() - slice.startTime}`);\n }\n selectiveDebug() && console.debug(`JobManager.workRemaining: queuedTime ${queuedTime}, queuedCount ${this.queuedSlices.length}, workingTime ${workingTime}, workingCount ${this.workingSliceCount}`);\n return workingTime + queuedTime;\n }\n /**\n * Are there no working slices?\n * @type {boolean}\n */\n get isNotWorking ()\n {\n for (const slice of this.sliceInventory)\n if (slice.isReserved || slice.isWorking)\n return false;\n return true;\n }\n /**\n * Estimated count of working (or reserved) slices.\n * @type {number}\n */\n get workingSliceCount ()\n {\n let count = 0;\n for (const slice of this.sliceInventory)\n if (slice.isReserved || slice.isWorking)\n ++count;\n return count;\n }\n /**\n * Estimated density of working (or reserved) slices.\n * @type {number}\n */\n get workingSliceDensity () { return this.workingSliceCount * this.estimateDensity; }\n /**\n * Estimated density of working (or reserved) slices.\n * @type {number}\n */\n get workingGPUDensity () { return this.workingSliceCount * this.estimateGPUDensity; }\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 * When displayMaxDiagInfo is true, display stack trace and other enhanced diag info.\n * @type {boolean}\n **/\n get displayMaxDiagInfo ()\n {\n return common.displayMaxDiagInfo() || this.workerConsole && this.supervisor.options.allowConsoleAccess;\n }\n\n // _Idx\n //\n // addSlices, fetchDependencies, updateStatistics, update, predictLoad\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 {AuthMessage} authorizationMessage - The signature that shipped with the task authorizing this worker.\n * @returns {Promise<*>}\n */\n addSlices (sliceMessages, authorizationMessage)\n {\n /** @type {Array<Promise<void>>} */\n const slicePromises = [];\n sliceMessages.forEach((sliceMessage) => {\n const slice = new Slice(this, sliceMessage, authorizationMessage);\n if (!slice.isEstimation)\n this.isEstimation = false;\n this.sliceInventory.push(slice);\n slicePromises.push(slice.fetchSliceDataPromise);\n });\n return Promise_any(slicePromises);\n }\n\n /**\n * Fetch dependencies.\n * @param {SliceMessage[]} sliceMessages - Messages from task distributor describing slices.\n * @param {AuthMessage} authorizationMessage - The signature that shipped with the task authorizing this worker.\n * @returns {Promise<*>}\n */\n fetchDependencies (sliceMessages, authorizationMessage)\n {\n return this.fetchJob(this.jobMessage)\n .then (() => {\n debugging('supervisor') && console.debug('JobManager is transitioning to READY', this.identifier, Date.now() - this.supervisor.lastTime);\n this.state.set(INITIAL, READY);\n return this.addSlices(sliceMessages, authorizationMessage);\n }, (error) => {\n selectiveDebug() && console.error('fetchJob has failed', error);\n this.state.set(INITIAL, BROKEN);\n throw error;\n });\n }\n\n /** XXXpfr @todo Factor out statistics and EMA into another class. */\n /**\n * @param {number} x\n * @param {boolean} isDensity\n * @returns {boolean}\n */\n static isValid (x, isDensity = false)\n {\n return Number.isFinite(x) && !(x < 0) && !(isDensity && x > 1);\n }\n\n /**\n * Old legacy DCP schedulers can send a corrupt this.metrics .\n * We should get rid of this when we don't support old legacy DCP schedulers.\n */\n fixUpMetrics ()\n {\n if (!this.metrics)\n this.jobMessage.metrics = { sliceCPUTime: 0, sliceCPUDensity: 1, sliceGPUTime: 0, sliceGPUDensity: 0, sliceInDataSize: 0, sliceOutDataSize: 0 };\n else\n {\n if (!(JobManager.isValid(this.metrics.sliceCPUTime) && JobManager.isValid(this.metrics.sliceCPUDensity, true)))\n {\n this.metrics.sliceCPUTime = 1;\n this.metrics.sliceCPUDensity = 0.5;\n }\n if (!(JobManager.isValid(this.metrics.sliceGPUTime) && JobManager.isValid(this.metrics.sliceGPUDensity, true)))\n {\n this.metrics.sliceGPUTime = 1;\n this.metrics.sliceGPUDensity = 0.5;\n }\n if (!JobManager.isValid(this.metrics.sliceInDataSize))\n this.metrics.sliceInDataSize = 0;\n if (!JobManager.isValid(this.metrics.sliceOutDataSize))\n this.metrics.sliceOutDataSize = 0;\n }\n }\n\n /**\n * @param {Slice} slice\n * @param {Sandbox} sandbox\n * @returns {{CPUTime: number, GPUTime: number, CPUDensity: number, GPUDensity: number, InDataSize: number, OutDataSize: number}}\n */\n updateStatistics (slice, sandbox)\n {\n /**\n * @todo XXXpfr Make this only check when debugBuild, but not until the Sup2 churn has stopped.\n * @param {{CPUTime: number, GPUTime: number, CPUDensity: number, GPUDensity: number, InDataSize: number, OutDataSize: number}} metrics\n * @returns {boolean}\n */\n const isValidMetrics = (metrics) => {\n return JobManager.isValid(metrics.CPUTime) && JobManager.isValid(metrics.CPUDensity, true)\n && JobManager.isValid(metrics.GPUTime) && JobManager.isValid(metrics.GPUDensity, true)\n && JobManager.isValid(metrics.InDataSize) && JobManager.isValid(metrics.OutDataSize);\n }\n\n const timeReport = slice.timeReport;\n const dataReport = slice.dataReport;\n selectiveDebug() && console.debug('updateStatistics', slice.identifier, timeReport, dataReport);\n\n // Sanity\n if (!(timeReport.CPU >= 1))\n {\n timeReport.CPU = 1;\n timeReport.total += 1;\n }\n if (!(timeReport.total >= 1))\n timeReport.total = 1;\n\n // Construct metrics.\n const GPUTime = timeReport.webGPU + timeReport.webGL;\n if (this.useGPU)\n updateGPUStatistics(GPUTime);\n const metrics = {\n CPUTime: timeReport.CPU,\n GPUTime,\n CPUDensity: timeReport.CPU / timeReport.total,\n GPUDensity: GPUTime / timeReport.total,\n InDataSize: dataReport.InDataSize,\n OutDataSize: dataReport.OutDataSize,\n };\n //console.debug('JM.updateStatistics metrics:', metrics, timeReport.total);\n\n // Create the measurement object which will be passed to the metrics event.\n const eventMeasurements = {\n elapsed: timeReport.total / 1000,\n CPU: metrics.CPUTime / 1000,\n GPU: metrics.GPUTime / 1000,\n in: metrics.InDataSize,\n out: metrics.OutDataSize,\n };\n\n // Emit metrics event for both jobHandle and sandboxHandle\n this.jobEmit( 'metrics', slice.sliceNumber, eventMeasurements);\n this.sandboxEmit(sandbox, 'metrics', slice.sliceNumber, eventMeasurements);\n selectiveDebug() && console.debug(`updateStatistics: slice ${slice.identifier}, eventMeasurements ${stringify(eventMeasurements)}`);\n\n if (!isValidMetrics(metrics))\n throw new Error(`JobManager.updateStatistics: metrics are in an inconsistent state ${JSON.stringify(metrics)}`);\n\n if (!this.localMetrics)\n this.localMetrics = { CPUTime: 0, GPUTime: 0, TotalTime: 0, CPUDensity: 0, GPUDensity: 0, InDataSize: 0, OutDataSize: 0 };\n\n // measuredSlices+1 because the current slice hasn't been measured yet in RS.updateMetrics.\n const alpha = this.metrics?.lastSliceNumber\n ? constructAlpha(this.metrics.lastSliceNumber, this.metrics.measuredSlices + 1)\n : 0.5;\n selectiveDebug2() && console.debug(`updateStatistics: local alpha ${alpha}, global alpha ${this.metrics?.alpha}`);\n this.localMetrics.CPUTime = nextEma(this.localMetrics.CPUTime, metrics.CPUTime, alpha);\n this.localMetrics.GPUTime = nextEma(this.localMetrics.GPUTime, metrics.GPUTime, alpha);\n this.localMetrics.TotalTime = nextEma(this.localMetrics.TotalTime, timeReport.total, alpha);\n this.localMetrics.CPUDensity = nextEma(this.localMetrics.CPUDensity, metrics.CPUDensity, alpha);\n this.localMetrics.GPUDensity = nextEma(this.localMetrics.GPUDensity, metrics.GPUDensity, alpha);\n this.localMetrics.InDataSize = nextEma(this.localMetrics.InDataSize, metrics.InDataSize, alpha); // Don't need this yet.\n this.localMetrics.OutDataSize = nextEma(this.localMetrics.OutDataSize, metrics.OutDataSize, alpha); // Don't need this yet.\n\n if (common.debugQuanta())\n {\n this.supervisor.dbg.addRawLocal(slice, metrics, alpha);\n this.supervisor.dbg.addLocal(slice, this.emaSliceTime, alpha);\n }\n\n if (!isValidMetrics(this.localMetrics))\n throw new Error(`JobManager.updateStatistics: localMetrics are in an inconsistent state ${JSON.stringify(this.localMetrics)}`);\n\n debugging('supervisor') && console.debug('updateStatistics: ema', this.statistics.ema, 'emaSliceTime', this.emaSliceTime, 'estimateDensity', this.estimateDensity, 'metrics', metrics, 'g-metrics', this.metrics, 'alpha', alpha);\n //if (!Number.isFinite(this.statistics.ma))\n // console.log(this.statistics.x);\n return metrics;\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 getJobsForTask.\n * @param {SliceMessage[]} [sliceMessages] - Messages from task distributor describing slices.\n * @param {AuthMessage} [authorizationMessage] - The signature that shipped with the task authorizing this worker.\n * @returns {Promise<*>}\n */\n update (jobMessage, sliceMessages, authorizationMessage)\n {\n Object.assign(this.jobMessage, jobMessage);\n /*\n * Make sure this.metrics contains valid data.\n * With old legacy schedulers, this may not be so.\n */\n this.fixUpMetrics();\n selectiveDebug2() && console.debug('JM.update: new this.jobMessage', this.jobMessage.metrics);\n\n if (sliceMessages)\n return (this.#jobPromise = this.addSlices(sliceMessages, authorizationMessage));\n }\n\n /**\n * Predict the total reduction in density of working sandboxes timeSpanMs from now.\n * This function is called right before fetchTask, in order to calculate how much space is available.\n * In pratice, the returned property queued will have length <= 1, because when it is more we won't call fetchWork.\n * @param {number} timeSpanMs\n * @returns {{ queued: Slice[], working: number }}\n */\n predictLoad (timeSpanMs)\n {\n let queuedCount = this.queuedSlices.length;\n // Optimize to short-circuit when queued > 1, because we won't call fetchWork in that case.\n if (queuedCount > this.workingSliceCount + 1)\n {\n selectiveDebug() && console.debug('JM.predictLoad: bail early', queuedCount, this.workingSliceCount);\n return { queued: [null, null], working: this.workingSliceDensity };\n }\n let space = 0;\n // 1) Predict the current working slices that will finish in timeSpanMs.\n // 2) Compensate for queued slices that may begin working.\n for (const slice of this.sliceInventory)\n {\n if (!slice.isReserved && !slice.isWorking)\n continue;\n const remainingTime = Math.max(0, this.emaSliceTime - (Date.now() - slice.startTime));\n selectiveDebug2() && console.debug('JM.predictLoad: predicting...', queuedCount, space, this.emaSliceTime, remainingTime, this.emaSliceTime <= timeSpanMs - remainingTime);\n // If slice will end before timeSpanMs elapses, it may open up space.\n if (remainingTime <= timeSpanMs)\n {\n // Check to see whether a new queued slices can start running and finish in the remaining time.\n // Somewhat inaccurate because it's possible multiple slices could finish in the remaining time.\n // e.g. Suppose timeSpanMs = 5 * 1000 and this.emaSliceTime is 1000, then there's room\n // to successively schedule 5 slices, one after another.\n /** XXXpfr @todo Fix to incorporate successive scheduling. */\n if (queuedCount > 0)\n {\n --queuedCount;\n // If the new slice finishes in time, count it as opening up space.\n if (this.emaSliceTime <= timeSpanMs - remainingTime)\n ++space;\n }\n else // No queued slice can run so space has opened up.\n ++space;\n }\n }\n // Optimize to short-circuit when queued > 1, because we won't call fetchWork in that case.\n let queued = [null, null];\n if (queuedCount < 2)\n queued = (queuedCount === 1) ? [this.queuedSlices[0]] : [];\n return { queued, working: (this.workingSliceCount - space) * this.estimateDensity };\n }\n\n // _Idx\n //\n // reserveOneSlice, resetSlices, destroy, evaluatorDownCleanup\n //\n\n /**\n * Find one ready slice, mark it as working and return.\n * If there are no ready slices return undefined.\n * @returns {Slice}\n */\n reserveOneSlice ()\n {\n for (const slice of this.sliceInventory)\n if (slice.isReady)\n return slice.markAsReserved();\n return null;\n }\n\n /**\n * Error recovery of slices.\n * When evaluator is down and it goes up again, there may be\n * working slices from working sandboxes that were terminated.\n * @param {string} [tag]\n */\n resetSlices (tag)\n {\n this.sliceInventory.forEach((slice) => {\n if (this.evaluatorDownHandle)\n this.evaluatorDownHandle.intr(); // Interrupt a$sleep in evaluatorDownCleanup\n this.evaluatorDownHandle = null;\n if (slice.isWorking || slice.isFailed)\n slice.resetState();\n });\n //this.dumpSlices(tag); // SAVE\n }\n\n /**\n * Destructor.\n * @returns {Promise<*>}\n */\n destroy ()\n {\n selectiveDebug() && console.debug(`JobManager.destroy: terminating sandboxes and returning slices to scheduler for job manager ${this.identifier}.`);\n this.sandboxInventory.forEach((sandbox) => { this.supervisor.returnSandbox(sandbox); });\n const reason = 'JobManager destroy';\n return this.supervisor.returnSlices(this.unFinishedSlices, reason)\n .finally (() => {\n this.#sliceInventory = [];\n this.state.removeAllListeners();\n if (this.state.isNot(BROKEN))\n this.state.set([ INITIAL, READY, STOP, REFUSE ], BROKEN);\n this.jobEmit('flush');\n });\n }\n\n /**\n * Destructor.\n * Called when the evaluator goes down (or screensaver goes up), in the sandbox 'terminated' event handler in Supervisor.\n * A slice marked as COMPLETE but not FINISHED means it still needs to submit results.\n * This function allows such slices to still submit results.\n * Because sometimes a screensaver only goes down for a minute or less (e.g. the user needs to look at something quick),\n * we delay an average of 90 (or 1.5 * delay) seconds before returning non-complete slices.\n * param {number} [delay=60] - Average delay is 1.5 * delay seconds.\n */\n evaluatorDownCleanup (delay = 60)\n {\n selectiveDebug() && console.debug(`JobManager.evaluatorDownCleanup: delay returning non-complete slices for on average ${1.5 * delay} seconds, ${this.identifier}.`);\n if (this.sandboxInventory.length > 0)\n this.warning('JobManager.evaluatorDownCleanup: When the evaluator is down there should not be any non-terminated sandboxes left.');\n // Call retVal.intr() to interrupt and cancel the timer inside a$sleepMs.\n return (this.evaluatorDownHandle = a$sleepMs(delay * (1.0 + Math.random()) * 1000)\n .finally(() => {\n const slicesToReturn = this.sliceInventory.filter((slice) => !(slice.isComplete || slice.isFinished));\n const reason = 'JobManager evaluatorDownCleanup';\n return this.supervisor.returnSlices(slicesToReturn, reason);\n }));\n }\n\n // _Idx\n //\n // assignSandbox, runSlice, runSliceOnSandbox\n // Functions chain and return non-awaited promises.\n //\n\n /**\n * Create a Sandbox.\n * Start it. Assign it. Add it to jobManager inventory.\n * @param {Slice} slice\n * @returns {Promise<Sandbox>}\n */\n async assignSandbox (slice)\n {\n try\n {\n // We want to await createSandbox so we don't create a large number of sandboxes all at once.\n const sandbox = await this.supervisor.createSandbox()\n .catch((error) => {\n // Catches exception from createSandbox.\n // Shound be rare unless evaluator or screensaver goes down.\n selectiveDebug2() && !this.supervisor.evaluator.down && console.debug('assignSandbox: createSandbox error', slice.identifier, error);\n slice.unReserve(); // reuse slice\n throw error;\n });\n selectiveDebug2() && console.debug('assignSandbox', slice.identifier);\n sandbox.slice = slice; // Must be before sandbox.assign; cf. fetchModule exception in handleRing2Message.\n return sandbox.assign(this)\n .then((assignedSandbox) => {\n selectiveDebug2() && console.debug('assignSandbox: success', sandbox.identifier);\n this.supervisor.sandboxInventory.push(assignedSandbox);\n return assignedSandbox;\n })\n .catch((error) => {\n // Exception is from sandbox.assign.\n // It could be a string coming from evaluator.\n this.error(`Failed to assign job to sandbox ${sandbox.identifier}`, error);\n slice.unReserve(); // reuse slice\n throw error;\n });\n }\n catch (error)\n {\n this.error(`JobManager.assignSandbox failed to find sandbox to run slice ${slice.identifier}`, error);\n throw error;\n }\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 * @returns {Promise<any>}\n */\n runSlice (slice)\n {\n try\n {\n selectiveDebug() && console.debug('runSlice:', slice.identifier, 'assigned/working', this.assignedSandboxes.length, this.workingSandboxes.length);\n\n if (this.supervisor.sliceTiming)\n slice['queueingDelta'] = Date.now();\n\n const _assignedSandboxes = this.assignedSandboxes;\n if (_assignedSandboxes.length > 0)\n {\n const sandbox = _assignedSandboxes[0];\n selectiveDebug2() && console.debug(`runSlice(assigned): sandbox ${sandbox.identifier}`, Date.now() - this.supervisor.lastTime);\n // When evaluator goes down, all sandboxes are terminated.\n // A ready slice has a finite lifetime and if exceeded may transition back to READY, in which\n // case we bail since it might already be in the process of being selected in roundRobinSlices.\n if (slice.isReady || sandbox.isTerminated)\n return Promise.resolve();\n slice.markAsWorking();\n sandbox.markAsWorking();\n sandbox.slice = slice; // Reusing assigned sandbox, so set the slice.\n return this.runSliceOnSandbox(slice, sandbox)\n .catch((error) => {\n // Catches exception from runSliceOnSandbox; s.b. rare.\n this.error(`JobManager.runSlice: Failure trying to run slice ${slice.identifier} on a assigned sandbox.`, error);\n return this.supervisor.returnSlice(slice, 'JobManager.runSlice');\n });\n }\n\n return this.assignSandbox(slice)\n .then((sandbox) => {\n selectiveDebug2() && console.debug(`runSlice: sandbox ${sandbox.identifier}`, Date.now() - this.supervisor.lastTime);\n // When evaluator goes down, all sandboxes are terminated.\n // A ready slice has a finite lifetime and if exceeded may transition back to READY, in which\n // case we bail since it might already be in the process of being selected in roundRobinSlices.\n if (slice.isReady || sandbox.isTerminated)\n return Promise.resolve();\n slice.markAsWorking();\n sandbox.markAsWorking();\n return this.runSliceOnSandbox(slice, sandbox);\n }, (error) => {\n // Catches exception from assignSandbox for error control flow.\n selectiveDebug() && console.error(`JobManager.runSlice: Failure from assignSandbox when trying to run slice ${slice.identifier} on a sandbox.`, error);\n })\n .catch((error) => {\n // Catches exception from runSliceOnSandbox; s.b. rare.\n this.error(`JobManager.runSlice: Failure trying to run slice ${slice.identifier} on a sandbox.`, error);\n return this.supervisor.returnSlice(slice, 'JobManager.runSlice');\n });\n }\n catch (error)\n {\n this.error(`JobManager.runSlice failed to run slice ${slice.identifier}`, error);\n return this.supervisor.returnSlice(slice, 'JobManager.runSlice_static_catch');\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 * @returns {Promise<any>}\n */\n runSliceOnSandbox (slice, sandbox)\n {\n const handleError = (error, tag) => {\n if (sandbox.isTerminated)\n {\n this.error(`The screensaver or evaluator may be down: slice ${slice.identifier}`, error);\n return this.supervisor.returnSlice(slice, `${tag}: sandbox terminated`);\n }\n else\n {\n this.error(`Failure in ${tag} for slice ${slice.identifier}`, error);\n const reason = (error.code === 'EPERM_ORIGIN' || error.code === 'EFETCH_BAD_ORIGIN' || error.code === 'EFETCH') ? error.code : tag;\n this.supervisor.returnSlice(slice, reason);\n }\n };\n debugging('supervisor') && console.debug(`runSliceOnSandbox ${sandbox.identifier} #(r/rsv/w/wsbx/sbx), ${this.supervisor.dbg.workingSliceSandboxStr}, #assigned, ${this.assignedSandboxes.length}, #localSBs, ${this.sandboxInventory.map(s => Number(s.id)).sort((x,y)=>x-y)}`);\n selectiveDebug() && console.debug('runSliceOnSandbox', sandbox.identifier, this.workingSliceCount, Date.now() - this.supervisor.lastTime);\n\n if (sandbox.isTerminated) // When evaluator goes down, all sandboxes are terminated.\n return Promise.resolve();\n\n this.sandboxEmit(sandbox, 'slice', slice.sliceNumber);\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 return sandbox.work()\n .finally(() => {\n if (this.supervisor.sliceTiming)\n {\n slice['executionDelta'] = Date.now() - slice['executionDelta'];\n slice['resultDelta'] = Date.now();\n }\n })\n .then((result) => {\n selectiveDebug2() && console.debug(`runSliceOnSandbox: sandbox${sandbox.id} - success: ${slice.identifier}`, Boolean(result), Date.now() - slice.startTime);\n\n if (sandbox.isTerminated)\n throw new Error(`Slice ${slice.identifier} completed work, but the sandbox ${sandbox.id} is terminated.`);\n\n // Allow reuse of sandbox up to a limit of dcpConfig.supervisor.sandbox.maxSandboxUse .\n sandbox.checkSandboxReUse();\n // Transition slice to COMPLETE\n slice.collectResult(result, true /*success*/);\n\n // Record result.\n return this.supervisor.recordResult(slice, sandbox)\n .catch((error) => {\n // Catches exception from supervisor.recordResult .\n return handleError(error, 'recordResult');\n });\n\n }, (error) => {\n // Catches exception from sandbox.work.\n // Kills sandbox and either retries the slice or transitions slice to FAILED and returns to scheduler.\n const reason = this.supervisor.handleSandboxWorkError(sandbox, slice, error);\n selectiveDebug() && console.error(`runSliceOnSandbox: Failure executing slice on sandbox: ${sandbox.identifier}`, reason, error);\n })\n .catch((error) => {\n // Catches exception from sandbox.work or then-clause.\n return handleError(error, 'runSliceOnSandbox');\n });\n }\n\n // _Idx\n //\n // removeSlice\n //\n\n /**\n * Remove slice from this.sliceInventory.\n * @param {Slice} slice - The slice to remove from this.sliceInventory.\n * @return {boolean} - False when slice is not in this.sliceInventory.\n */\n removeSlice (slice)\n {\n debugging('supervisor') && console.debug(`removeSlice slice ${slice.sliceNumber}`);\n return common.removeElement(this.sliceInventory, slice);\n }\n\n // _Idx\n //\n // fetchError, fetchJob, fetchSliceData\n //\n\n fetchError (error)\n {\n if (error.code === 'EPERM_ORIGIN')\n throw new BadOriginError(error);\n if (error.code === 'EFETCH')\n throw new RemoteFetchError(error);\n throw new UncaughtExceptionError(error);\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 * @returns {Promise<*>}\n */\n fetchJob (mpe)\n {\n debugging('supervisor') && utils.dumpObject(mpe, 'JobManager.fetchJob: mpe', 512);\n\n const promises = [];\n try\n {\n // Get workFn.\n if (!mpe.workFunction)\n {\n const workFunctionPromise = this.fetchWorkFunctions(mpe.codeLocation)\n .then((workFunction) => {\n this.inputDataSize += workFunction.size;\n mpe.workFunction = workFunction.data;\n if (mpe.requirements.useStrict)\n mpe.useStrict = true;\n delete mpe.codeLocation;\n }, (error) => {\n this.fetchError(error);\n });\n promises.push(workFunctionPromise);\n }\n\n if (!mpe.mro && mpe.MROLocation)\n {\n const mroPromise = this.fetchData(mpe.MROLocation)\n .then((mro) => {\n // It's not ready for computing the inputDataSize yet -- that's done in JobManager.fetchSliceData .\n mpe.mro = mro.data;\n delete mpe.MROLocation;\n }, (error) => {\n this.fetchError(error);\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(this.fetchArguments(mpe.argumentsLocation[k].value)\n .then((arg) => {\n this.inputDataSize += arg.size;\n mpe.arguments[k] = arg.data;\n }, (error) => {\n this.fetchError(error);\n })\n );\n }\n\n return Promise.all(promises)\n .then(() => {\n if (mpe.argumentsLocation)\n delete mpe.argumentsLocation;\n return mpe;\n }, (error) => {\n this.fetchError(error);\n });\n }\n catch (error)\n {\n this.fetchError(error);\n }\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<{ data: *, size: number }>}\n */\n fetchSliceData (datumUri, slice)\n {\n try\n {\n if (!datumUri)\n {\n if (!this.mro)\n throw new Error('Must complete call to JobManager.fetchJob before calling JobManager.fetchSliceData.');\n /** XXXpfr @todo Inefficient, we're rehydrating the whole range for a single slice datum. */\n const ro = rehydrateRange(this.mro);\n // Slice numbers start at 1.\n const sliceDatum = ro[slice.sliceNumber - 1];\n debugging('supervisor') && console.debug(`Fetched mro datum: ${stringify(sliceDatum, 512)}`);\n return Promise.resolve({ data: sliceDatum, size: JSON.stringify(sliceDatum).length });\n }\n\n return this.fetchData(datumUri)\n .catch((error) => {\n throw this.fetchError(error);\n });\n }\n catch (error)\n {\n this.fetchError(error);\n }\n }\n\n // _Idx\n //\n // jobEmit, sandboxEmit, error, warning. countSliceStr, dumpSlices\n //\n\n /**\n * Safe event emitter on jobHandle.\n * @param {string} event\n * @param {...any} args\n */\n jobEmit(event, ...args)\n {\n this.supervisor.safeEmit(this.jobHandle, event, ...args);\n }\n\n /**\n * Safe event emitter on sandboxHandle.\n * @param {Sandbox} sandbox\n * @param {string} event\n * @param {...any} args\n */\n sandboxEmit(sandbox, event, ...args)\n {\n this.supervisor.safeEmit(sandbox.sandboxHandle, event, ...args);\n }\n\n /**\n * Error feedback to user.\n * @param {string} message\n * @param {Array<Error>|Error|string} [coreError]\n * @param {string} [additionalInfo]\n */\n error (message, coreError, additionalInfo)\n {\n this.supervisor.error(message, coreError, additionalInfo);\n }\n\n /**\n * Warning feedback to user.\n * @param {string[]} messages\n */\n warning (...messages)\n {\n this.supervisor.warning(...messages);\n }\n\n /**\n * Debugging helper.\n * @param {string} [tag='']\n * @returns {string}\n */\n countSliceStr (tag)\n {\n const { unassigned, ready, reserved, working, workdone, complete, failed, finished } = this.dumpSlices (tag, false, false);\n return `${tag}: u/r/rsv/w/wd/c/f/fsh ${unassigned}/${ready}/${reserved}/${working}/${workdone}/${complete}/${failed}/${finished}`;\n }\n\n /**\n * @param {URL|string} uri\n * @returns {Promise<*>}\n */\n fetchData (uri) { return fetchURIAndLength(uri, this.originManager, allowOriginsPurposes.fetchData); }\n /**\n * @param {URL|string} uri\n * @returns {Promise<*>}\n */\n fetchArguments (uri) { return fetchURIAndLength(uri, this.originManager, allowOriginsPurposes.fetchArguments); }\n /**\n * @param {URL|string} uri\n * @returns {Promise<*>}\n */\n fetchWorkFunctions (uri) { return fetchURIAndLength(uri, this.originManager, allowOriginsPurposes.fetchWorkFunctions); }\n\n /**\n * Debugging helper.\n * @param {string} [tag='']\n * @param {boolean} [details=false]\n * @param {boolean} [display=true]\n * @returns {{ ready, reserved, working, workdone, complete, failed, finished, unassigned }}\n */\n dumpSlices (tag = '', details = false, display = true)\n {\n if (display)\n console.log(`dumpSlices(${tag}): ${this.identifier}, sliceCount ${this.sliceInventory.length}, sandboxCount ${this.sandboxInventory.length}`, Date.now() - this.supervisor.lastTime);\n if (this.sliceInventory.length < 1) return { unassigned:0, ready:0, reserved:0, working:0, workdone:0, complete:0, failed:0, finished:0 };\n if (details)\n {\n // Detailed classification of slices.\n const unassigned = [], ready = [], reserved = [], working = [], workdone = [], complete = [], failed = [], finished = [];\n for (const slice of this.sliceInventory)\n {\n if (slice.isReady)\n ready.push(slice);\n else if (slice.isReserved)\n reserved.push(slice);\n else if (slice.isWorking)\n working.push(slice);\n else if (slice.isWorkDone)\n workdone.push(slice);\n else if (slice.isComplete)\n complete.push(slice);\n else if (slice.isFailed)\n failed.push(slice);\n else if (slice.isFinished)\n finished.push(slice);\n else if (slice.isUnassigned)\n unassigned.push(slice);\n else\n throw new Error(`dumpSlices: Unexpected kind a slice; ${slice.identifier}`);\n }\n if (display)\n {\n const dmpSlices = (name, slices) => {\n console.log(`-----${name}(${slices.length})-----------------------------------------------------------------`);\n for (const slice of slices)\n console.log(slice.identifier);\n }\n dmpSlices('unassigned', unassigned);\n dmpSlices('ready', ready);\n dmpSlices('reserved', reserved);\n dmpSlices('working', working);\n dmpSlices('workdone', workdone);\n dmpSlices('complete', complete);\n dmpSlices('failed', failed);\n dmpSlices('finished', finished);\n console.log('-----------------------------------------------------------------------------------');\n }\n return {\n ready: ready.length,\n reserved: reserved.length,\n working: working.length,\n workdone: workdone.length,\n complete: complete.length,\n failed: failed.length,\n finished: finished.length,\n unassigned: unassigned.length,\n };\n }\n else\n {\n // Quick classification of slices.\n const { ready,\n reserved,\n working,\n workdone,\n complete,\n failed,\n finished,\n unassigned } = common.sliceCounts(this.sliceInventory);\n if (display)\n {\n const dumpCount = (name, count) => {\n console.log(`-----${name}(${count})-----------------------------------------------------------------`);\n }\n dumpCount('unassigned', unassigned);\n dumpCount('ready', ready);\n dumpCount('reserved', reserved);\n dumpCount('working', working);\n dumpCount('workdone', workdone);\n dumpCount('complete', complete);\n dumpCount('failed', failed);\n dumpCount('finished', finished);\n console.log('-----------------------------------------------------------------------------------');\n }\n return { ready, reserved, working, workdone, complete, failed, finished, unassigned };\n }\n }\n\n}\nexports.JobManager = JobManager;\n\n\n//# sourceURL=webpack://dcp/./src/dcp-client/worker/supervisor2/job-manager.js?");
|
|
4739
4739
|
|
|
4740
4740
|
/***/ }),
|
|
4741
4741
|
|
|
@@ -4798,7 +4798,7 @@ eval("/**\n * @file dcp/src/dcp-client/worker/supervisor2/rolling-statistics.js\
|
|
|
4798
4798
|
/***/ ((__unused_webpack_module, exports, __webpack_require__) => {
|
|
4799
4799
|
|
|
4800
4800
|
"use strict";
|
|
4801
|
-
eval("/**\n * @file dcp/src/dcp-client/worker/supervisor2/sandbox2.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, { ...this.options.sandboxOptions });\n * await sandbox.start();\n * sandbox.slice = slice;\n * await sandbox.assign(jobManager);\n * return sandbox.work()\n * .then((result) => {\n * slice.collectResult(result, true);\n * sandbox.checkSandboxReUse();\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 * Initial states:\n * UNREADY\n *\n * Terminal states:\n * TERMINATED\n *\n * Valid transitions:\n * ( sandbox.start )\n * UNREADY -> READYING -> READY_FOR_ASSIGN\n * READYING -> TERMINATED\n * ( sandbox.assign )\n * READY_FOR_ASSIGN -> ASSIGNING -> ASSIGNED\n * ASSIGNING -> TERMINATED\n * ( sandbox.markAsWorking )\n * ASSIGEND -> WORKING\n * ( sandbox.work )\n * WORKING -> ASSIGNED\n * -> TERMINATED\n * ( sandbox.terminate )\n * any -> TERMINATED\n *\n * @author Matthew Palma, mpalma@kingsds.network\n * Ryan Rossiter, ryan@kingsds.network\n * Wes Garland, wes@distributive.network\n * Paul, paul@distributive.network\n * @date May 2019\n * May 2019\n * Decemeber 2020\n * June, Dec 2022, Jan-May 2023\n * @module sandbox\n * @copyright Copyright (c) 2018-2023, Distributive Corp. All Rights Reserved\n */\n// @ts-check\n\n\nconst debugging = (__webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope)('worker');\nconst dcp_timers = __webpack_require__(/*! dcp/common/dcp-timers */ \"./src/common/dcp-timers.js\");\nconst { assert, assertEq3 } = __webpack_require__(/*! dcp/common/dcp-assert */ \"./src/common/dcp-assert.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__(/*! events */ \"./node_modules/events/events.js\");\nconst kvin = __webpack_require__(/*! kvin */ \"./node_modules/kvin/kvin.js\");\nconst { Config } = __webpack_require__(/*! ./config */ \"./src/dcp-client/worker/supervisor2/config.js\");\nconst common = __webpack_require__(/*! ./common */ \"./src/dcp-client/worker/supervisor2/common.js\");\nconst { selectiveDebug, truncateAddress, timeDilation, selectiveDebug2 } = common;\nconst { stringify } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\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// 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 TERMINATED = 'TERMINATED' // Sandbox is terminated.\nconst EVAL_RESULT_PREFIX = 'evalResult::';\n\nclass SandboxError extends Error\n{\n /**\n * @param {string} errorCode\n * @param {string|Error} msg\n */\n constructor(errorCode, msg)\n {\n super((msg.constructor?.name === 'String') ? msg : msg['message']);\n /** @type {string} */\n this.errorCode = errorCode;\n if (msg.constructor?.name !== 'String')\n for (const prop of [ 'name', 'code', 'stack', 'lineNumber', 'columnNumber' ])\n if (msg[prop]) this[prop] = msg[prop];\n }\n}\nclass NoProgressError extends SandboxError { constructor(msg) { super('ENOPROGRESS', msg); } }\nclass SliceTooSlowError extends SandboxError { constructor(msg) { super('ESLICETOOSLOW', msg); } }\nclass UncaughtExceptionError extends SandboxError { constructor(msg) { super('EUNCAUGHT', msg); } }\n\n/** @typedef {import('./slice2').Slice} Slice */\n/** @typedef {import('./index').Supervisor} Supervisor */\n/** @typedef {import('./job-manager').JobManager} JobManager */\n/** @typedef {import('./module-cache').ModuleCache} ModuleCache */\n/** @typedef {import('dcp/utils/jsdoc-types').SandboxOptions} SandboxOptions */\n\n/**\n * Public event emitter.\n * https://gitlab.com/Distributed-Compute-Protocol/dcp-docs-wes/-/blob/wip/worker/worker-events.md\n */\nclass SandboxHandle extends EventEmitter\n{\n /** @type {{ id: number, public: { name: string, description: string, link: string }, jobManager: JobManager, slice: Slice }} */\n #info;\n\n /**\n * @constructor\n * @param {Sandbox} sandbox\n */\n constructor (sandbox)\n {\n super({ captureRejections: false });\n this.#info = sandbox.info;\n }\n /** @type {number} */\n get id () { return this.#info.id; }\n /** @type {{ name: string, description: string, link: string }} */\n get public () { return this.#info.public ?? { name: '<unassigned>', description: '', link: '' }; }\n /** @type {string} */\n get jobAddress () { return this.#info.jobManager?.address; }\n /** @type {number} */\n get sliceNumber () { return this.#info.slice?.sliceNumber ?? -1; }\n}\nexports.SandboxHandle = SandboxHandle;\n\n//\n// Index to functionality -- search for '_Idx' to toggle through the index.\n//\n// 1) class Sandbox\n// 2) checkSandboxReUse, postMessageToEvaluator, changeState,\n// and punctuatedTimer is expiremental for replacing hard-coded timeouts.\n// 3) start, describe, assign, applyRequirements, assignEvaluator\n// 4) eval, resetState, work, resetProgressTimeout, resetSliceTimeout\n// 5) handleRing0Message, handleRing1Message, handleRing2Message, handleRing3Message\n// 6) onmessage, onerror, terminate\n// 7) updateTime, resetSliceReport, sandboxEmit, error, warning\n//\n\n// _Idx\n//\n// class Sandbox\n//\n\nclass Sandbox extends EventEmitter\n{\n /**\n * A Sandbox (i.e. a worker sandbox) which executes distributed slices.\n *\n * @constructor\n * @param {Supervisor} supervisor\n * @param {SandboxOptions} options\n */\n constructor (supervisor, options)\n {\n super({ captureRejections: false });\n /** @type {Supervisor} */\n this.supervisor = supervisor;\n /** @type {ModuleCache} */\n this.moduleCache = supervisor.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 {Synchronizer} */\n this.state = new Synchronizer(UNREADY, [ UNREADY, READYING, READY_FOR_ASSIGN, ASSIGNING, ASSIGNED, WORKING, TERMINATED ]);\n\n /** @type {{ id: number, public: { name: string, description: string, link: string }, jobManager: JobManager, slice: Slice }} */\n this.info = {\n id: Sandbox.getNewId(),\n public: null,\n jobManager: null,\n slice: null,\n };\n\n /**\n * Event emitter containing info that describes the sandbox.\n * @type {SandboxHandle}\n */\n this.sandboxHandle = new SandboxHandle(this);\n\n /** Properties of type object. */\n this.evaluatorHandle = null;\n this.capabilities = null;\n this.progressTimeout = null;\n this.sliceTimeout = null;\n this.rejectionData = null;\n\n /** @type {number?} */\n this.progress = 100;\n /** @type {{ last: { deltaMs: number, value: any, throttledReports: number }, lastDeterministic: { deltaMs: number, progress: number, value: any, throttledReports: number } }} */\n this.progressReports = null; // cf. job-noProgress.js\n /** @type {object} */\n this.progressTimeout = null;\n /** @type {object} */\n this.sliceTimeout = null;\n\n /** @type {{ total: number, CPU: number, webGL: number, webGPU: number }} */\n this.sliceTimeReport = null;\n /** @type {number} */\n this.moduleInDataSize = 0; // Sandbox level input size; set during assign, never reset.\n /** @type {number} */\n this.sliceOutDataSize = 0; // Slice level output size; reset for every slice executed.\n\n /** @type {number?} */\n this.sliceStartTime = null;\n /** @type {number} */\n this.useCounter = 1; // Anticipating the initial use.\n /** @type {Config} */\n this.hive = new Config();\n\n ///** @type {((data: any) => Promise<void>)[]} */\n this.ringMessageHandlers = [\n this.handleRing0Message,\n this.handleRing1Message,\n this.handleRing2Message,\n this.handleRing3Message,\n ];\n\n this.resetSliceReport();\n }\n\n /** @type {number} */\n get id () { return this.info.id; }\n /** @type {{ name: string, description: string, link: string }} */\n get public () { return this.info.public; }\n /** @type {{ name: string, description: string, link: string }} */\n set public (data) { this.info.public = data; }\n /** @type {JobManager} */\n get jobManager () { return this.info.jobManager; }\n /** @type {string} */\n get jobAddress () { return this.jobManager?.address; }\n /** @type {Slice} */\n get slice () { return this.info.slice; }\n /** @type {Slice} */\n set slice (slice) { this.info.slice = slice; }\n /** @type {number} */\n get sliceNumber () { return this.slice ? this.slice.sliceNumber : -1; }\n /** @type {number} */\n get generalTimeout () { return 2 * this.hive.generalTimeout; }\n /** @type {number} */\n get punctuatedTimeout () { return this.hive.generalTimeout; }\n\n /**\n * Debug string that characterizes sandbox.\n * @type {string}\n */\n get identifier()\n {\n if (!this.jobAddress)\n return `${this.id}.${this.state}`;\n const address = truncateAddress(this.jobAddress);\n if (this.slice)\n return `${this.id}.${address}.${this.state}~${this.slice.sliceNumber}`;\n return `${this.id}.${address}.${this.state}`;\n }\n\n /** @returns {number} */\n static getNewId() { return Sandbox.idCounter++; }\n\n /** @type {boolean} */\n get isReadyForAssign () { return this.state.is(READY_FOR_ASSIGN); }\n /** @type {boolean} */\n get isAssigned () { return this.state.is(ASSIGNED); }\n /** @type {boolean} */\n get isWorking () { return this.state.is(WORKING); }\n /** @type {boolean} */\n get isTerminated () { return this.state.is(TERMINATED); }\n\n // _Idx\n //\n // checkSandboxReUse, postMessageToEvaluator, changeState,\n // punctuatedTimer is expiremental for replacing hard-coded timeouts.\n //\n\n /**\n * Mark WORKING sandbox as ASSIGNED in preparation for possible reuse.\n * Allow use of sandbox on a given job up to a limit of dcpConfig.supervisor.sandbox.maxSandboxUse .\n */\n checkSandboxReUse ()\n {\n selectiveDebug2() && console.debug(`Sandbox2.checkSandboxReUse: useCounter ${this.useCounter}, ${this.identifier}`);\n if (this.useCounter++ < this.hive.maxSandboxUse)\n {\n this.state.set(WORKING, ASSIGNED);\n this.sandboxEmit('ready');\n }\n else\n {\n this.terminate(false);\n common.removeElement(this.supervisor.sandboxInventory, this);\n }\n }\n\n /** Transitions: ASSIGNED --> WORKING. */\n markAsWorking ()\n {\n if (!this.isAssigned)\n throw new Error(`Sandbox ${this.identifier} is not ready to work`);\n this.state.set(ASSIGNED, WORKING);\n }\n \n /**\n * Safely post message to evaluator.\n * @param {object} message\n */\n postMessageToEvaluator (message)\n {\n if (this.isTerminated) // When evaluator goes down, all sandboxes are terminated.\n throw new Error(`postMessageToEvaluator: Sandbox ${this.identifier} has been terminated.`);\n return this.evaluatorHandle.postMessage(message);\n }\n \n /**\n * Safely change state.\n * @param {string} currentState\n * @param {string} nextState\n */\n changeState (currentState, nextState)\n {\n if (this.isTerminated) // When evaluator goes down, all sandboxes are terminated.\n throw new Error(`changeState: Sandbox ${this.identifier} has been terminated.`);\n this.state.set(currentState, nextState);\n }\n\n /** Upon fatal error return slice to scheduler. */\n returnSlice ()\n {\n selectiveDebug() && console.debug('Sandbox.returnSlice', this.identifier);\n return this.supervisor.returnSlice(this.slice, 'Sandbox.returnSlice');\n }\n\n /**\n * @callback cbFn\n * @returns {void}\n */\n\n /**\n * UNUSED.\n * Future work.\n * Replaces the timers in:\n * describe,\n * applyRequirements,\n * resetState,\n * The idea is to have a long timeout with a warning every\n * 6 seconds saying why it is waiting.\n * @param {cbFn} body\n * @param {string} waitMessage\n * @param {string} timerExpiredMessage\n * @returns {Promise<{ closeIntervalTimer: cbFn }>}\n */\n punctuatedTimer(body, waitMessage, timerExpiredMessage)\n {\n const that = this;\n return new Promise((resolve, reject) => {\n let intervalCounter = 0;\n let intervalHandle = null;\n function closeIntervalTimer()\n {\n if (intervalHandle !== null)\n dcp_timers.clearTimeout(intervalHandle);\n intervalHandle = null;\n }\n intervalHandle = dcp_timers.setInterval(() => {\n if (++intervalCounter > 12)\n {\n closeIntervalTimer();\n that.error(timerExpiredMessage);\n }\n that.warning(waitMessage);\n body();\n }, this.punctuatedTimeout)\n // Allow workers and localExec to exit.\n intervalHandle.unref();\n resolve({ closeIntervalTimer });\n });\n }\n\n // _Idx\n //\n // start, describe, assign, applyRequirements, assignEvaluator\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 * Sandbox.start will terminate the sandbox upon failure.\n * @todo maybe preload specific modules or let the cache pass in what modules to load?\n *\n * @returns {Promise<void>}\n * @throws on failure to ready\n */\n async start ()\n {\n debug('Sandbox.start begin');\n await this.supervisor.delayManager.nextDelay('sandboxStart');\n this.changeState(UNREADY, READYING);\n\n try\n {\n // RING 0\n this.evaluatorHandle = new this.options.SandboxConstructor({\n name: `DCP Sandbox #${this.id}`,\n });\n // Annoying! onerror terminates sandbox which can happen independent of whether the slice\n // is ok or not. Since we don't know, we have to return the slice when onerror is called\n // during sandbox.work .\n /** @todo XXXpfr Beware of onerror firing often. */\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 const data = (event.data.serialized)\n ? kvin.parse(event.data.message)\n : 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(kvin.marshal(message));\n }\n\n const that = this;\n this.evaluatorHandle.addEventListener('end', function sandbox$start$addEventListener() {\n selectiveDebug() && console.debug(\"END:Sandbox evaluatorHandle end-handler\", that.identifier, new Date());\n that.supervisor.evaluator.shuttingDown = true;\n that.terminate(true);\n });\n\n // Don't let an open sockets prevent clean worker exit.\n if (this.evaluatorHandle.unref)\n this.evaluatorHandle.unref();\n\n // Now in RING 1\n\n // Now in RING 2\n await this.describe();\n this.changeState(READYING, READY_FOR_ASSIGN);\n\n // Emit the 'sandbox' event on the worker event emitter.\n this.supervisor.safeEmit(this.supervisor.worker, 'sandbox', this.sandboxHandle);\n }\n catch (error)\n {\n if (this.isTerminated)\n debug(`Failed to start sandbox because it is already terminated: ${this.identifier}.\\n\\tMay be due to screensaver worker being down or evaluator was stopped.`);\n else\n {\n debug(`Failed to start sandbox ${this.identifier}.`, error.message); // FIX s.b. error\n this.terminate(false);\n }\n throw error;\n }\n }\n\n /**\n * Sends a post message to describe its capabilities.\n * Side effect: Sets the capabilities property of the current sandbox.\n *\n * @returns {Promise<any>} Resolves with the sandbox's capabilities.\n * Rejects with an error saying a response was not received.\n * @memberof Sandbox\n */\n describe ()\n {\n debugging('sandbox') && debug('Beginning to describe evaluator', this.identifier);\n const that = this;\n\n return new Promise(function sandbox$describePromise(resolve, reject) {\n let describeTimeout;\n\n if (that.isTerminated) // When evaluator goes down, all sandboxes are terminated.\n reject(new Error(`Sandbox ${that.identifier} has been terminated.`));\n\n if (that.evaluatorHandle === null)\n reject(new Error(`Evaluator has not been initialized: ${that.identifier}`));\n\n function sandbox$describe$success(data)\n {\n if (describeTimeout !== false)\n {\n dcp_timers.clearTimeout(describeTimeout);\n describeTimeout = false;\n\n const { capabilities } = data;\n if (typeof capabilities === 'undefined')\n reject(new Error(`Did not receive capabilities from describe response: ${that.identifier}`));\n that.capabilities = capabilities;\n\n debugging('sandbox') && debug('Evaluator has been described');\n resolve(capabilities);\n }\n }\n // Emitted by handleRing2Message.\n that.once('describe', sandbox$describe$success);\n\n describeTimeout = dcp_timers.setTimeout(function sandbox$describe$fail() {\n if (describeTimeout !== false)\n {\n describeTimeout = false;\n that.removeListener('describe', sandbox$describe$success);\n reject(new Error( `Describe message timed-out. No describe response was received from the describe command: ${that.identifier}`));\n }\n }, that.generalTimeout);\n // Allow workers and localExec to exit.\n describeTimeout.unref();\n\n const message = {\n request: 'describe',\n };\n that.postMessageToEvaluator(message);\n });\n }\n\n /**\n * This will assign the sandbox with a job, loading its sandbox code into the sandbox.\n * Sandbox.assign will not terminate the sandbox upon failure.\n * The sandbox will be terminated in JobManager.assignSandbox .\n * @param {JobManager} jobManager - The job manager that will be the owner of this sandbox.\n * @returns {Promise<Sandbox>}\n * @throws on initialization failure\n */\n async assign (jobManager)\n {\n if (!this.slice) // Design assumption.\n throw new Error(`Must have valid sandbox.slice before sandbox.assign is called: ${this.identifier}`);\n\n await this.supervisor.delayManager.nextDelay('sandboxAssign');\n debug('Sandbox.assign', this.identifier, Date.now() - this.supervisor.lastTime);\n\n try\n {\n this.changeState(READY_FOR_ASSIGN, ASSIGNING);\n this.info.jobManager = jobManager;\n this.job = this.jobManager.jobMessage;\n\n /* At this point, the worker has decided that this sandbox will be associated with a specific job. \n Therefore, we emit the SandboxHandle<job> event*/\n this.sandboxEmit('job', jobManager.jobHandle);\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 ${truncateAddress(this.jobAddress)}`,\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.applyRequirements(this.job.requirements);\n //const _t0 = Date.now();\n await this.assignEvaluator();\n //console.log('Finished Sandbox.assignEvaluator', Date.now() - _t0);\n this.changeState(ASSIGNING, ASSIGNED);\n this.sandboxEmit('ready');\n }\n catch (error)\n {\n if (this.isTerminated)\n debug(`Failed to assign sandbox ${this.identifier} to evaluator because it is already terminated.\\n\\tMay be due to screensaver worker being down or evaluator was stopped.`);\n else\n {\n debug(`Failed to assign sandbox ${this.identifier} to evaluator.`);\n this.terminate(false);\n }\n throw error;\n }\n\n return this;\n }\n\n /**\n * Passes the job's requirements object into the sandbox so that the global access lists can be updated accordingly.\n * E.g. disallow access to OffscreenCanvas without environment.offscreenCanvas=true present.\n * Must be called after @start.\n *\n * @returns {Promise<void>} - resolves on success, rejects otherwise\n */\n applyRequirements (requirements)\n {\n assert(typeof requirements === 'object');\n const that = this;\n\n return new Promise(function sandbox$applyRequirementsPromise(resolve, reject) {\n let requirementTimeout;\n\n function sandbox$applyRequirements$success()\n {\n if (requirementTimeout !== false)\n {\n dcp_timers.clearTimeout(requirementTimeout);\n requirementTimeout = false;\n resolve();\n }\n }\n // Emitted by handleRing1Message.\n that.once('applyRequirementsDone', sandbox$applyRequirements$success);\n\n requirementTimeout = dcp_timers.setTimeout(function sandbox$finishApplySandboxRequirements$fail() {\n if (requirementTimeout !== false)\n {\n requirementTimeout = false;\n that.removeListener('applyRequirementsDone', sandbox$applyRequirements$success);\n reject(new Error(`applyRequirements never received 'applyRequirementsDone' response from sandbox: ${that.identifier}`));\n }\n }, that.generalTimeout);\n // Allow workers and localExec to exit.\n requirementTimeout.unref();\n\n const message = {\n requirements,\n request: 'applyRequirements',\n };\n that.postMessageToEvaluator(message);\n });\n }\n\n /**\n * Assign job to the evaluator.\n * @returns {Promise<any>} - resolves on success, rejects otherwise\n */\n assignEvaluator ()\n {\n debugging('sandbox') && console.debug('Begin assigning job to evaluator', this.identifier);\n const that = this;\n\n return new Promise(function sandbox$$assignEvaluatorPromise(resolve, reject) {\n function sandbox$assignEvaluator$success(event)\n {\n that.removeListener('reject', sandbox$assignEvaluator$fail);\n debugging('sandbox') && debug('Job assigned to evaluator');\n resolve(event);\n }\n\n function sandbox$assignEvaluator$fail(error)\n {\n that.removeListener('assigned', sandbox$assignEvaluator$success);\n that.error(`assignEvaluator failed(${that.identifier}): evaluator may be out of memory or the screensaver may be down.`, error);\n selectiveDebug() && console.debug('assignEvaluator failed', that.identifier, error);\n if (that.slice) // Normally the slice hasn't been set yet.\n that.returnSlice();\n reject(error);\n }\n\n // Emitted by handleRing2Message.\n that.once('assigned', sandbox$assignEvaluator$success);\n that.once('reject', sandbox$assignEvaluator$fail);\n\n // Had to add useStrict -- not sure if anything else was missed.\n const jobMessage = {\n address: that.job.address,\n arguments: that.job.arguments,\n dependencies: that.job.dependencies,\n modulePath: that.job.modulePath,\n public: that.job.public,\n requireModules: that.job.requireModules,\n requirePath: that.job.requirePath,\n workFunction: that.job.workFunction,\n useStrict: that.job.useStrict,\n };\n\n const message = {\n request: 'assign',\n job: jobMessage,\n sandboxConfig: that.hive.sandboxConfig,\n };\n that.postMessageToEvaluator(message);\n });\n }\n\n // _Idx\n //\n // eval, resetState, work, resetProgressTimeout, resetSliceTimeout\n //\n\n /**\n * Evaluates a string inside the sandbox.\n * @todo XXXpfr -- I don't understand how this gets called?\n * There's an old comment saying: \"no longer working though?\"\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 * @returns {Promise<any>} - resolves with eval result on success, rejects otherwise\n */\n eval (code, filename)\n {\n const that = this;\n const msgId = nanoid();\n\n return new Promise(function sandbox$$eval$Promise(resolve, reject) {\n const eventId = EVAL_RESULT_PREFIX + msgId;\n\n function sandbox$eval$success(event)\n {\n that.removeListener('reject', sandbox$eval$fail);\n resolve(event);\n };\n\n function sandbox$eval$fail(error)\n {\n that.removeListener(eventId, sandbox$eval$success);\n reject(error);\n };\n\n that.once(eventId, sandbox$eval$success);\n that.once('reject', sandbox$eval$fail);\n\n const message = {\n request: 'eval',\n data: code,\n filename,\n msgId,\n };\n that.postMessageToEvaluator(message);\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<void>} - resolves with result on success, rejects otherwise\n */\n resetState ()\n {\n const that = this;\n assert(this.isWorking); // Design assumption.\n\n return new Promise(function sandbox$resetStatePromise(resolve, reject) {\n let resetStateTimeout;\n\n function sandbox$resetState$success ()\n {\n if (resetStateTimeout !== false)\n {\n dcp_timers.clearTimeout(resetStateTimeout);\n resetStateTimeout = false;\n resolve();\n }\n }\n that.once('resetStateDone', sandbox$resetState$success);\n\n resetStateTimeout = dcp_timers.setTimeout(function sandbox$resetState$fail() {\n if (resetStateTimeout !== false)\n {\n resetStateTimeout = false;\n that.removeListener('resetStateDone', sandbox$resetState$success);\n reject(new Error(`resetState never received resetStateDone event from sandbox: ${that.identifier}`));\n }\n }, that.generalTimeout);\n // Allow workers and localExec to exit.\n resetStateTimeout.unref();\n\n const message = {\n request: 'resetState',\n };\n that.postMessageToEvaluator(message);\n });\n }\n\n /**\n * Executes a slice received from the supervisor.\n * Must be called after this.start, this.assign and this.markAsWorking .\n * Sandbox.work will not terminate the sandbox upon failure.\n * The sandbox will be terminated in Supervisor.handleSandboxWorkError .\n * @returns {Promise<any>} - resolves with result on success, rejects otherwise\n */\n async work ()\n {\n const that = this;\n\n if (!this.slice) // Design assumption\n throw new Error(`Must have valid sandbox.slice before sandbox.assign is called: ${this.identifier}`);\n\n await this.supervisor.delayManager.nextDelay('sandboxWork');\n debug('Sandbox.work begin', this.identifier, Date.now() - this.supervisor.lastTime);\n\n if (this.isTerminated) // When evaluator goes down, all sandboxes are terminated.\n throw new Error(`Sandbox ${this.identifier} has been terminated.`);\n if (!this.isWorking)\n throw new Error(`Sandbox ${this.identifier} in Sandbox.work must be marked as working.`)\n\n // cf. DCP-1719,1720\n this.resetSliceReport();\n\n // Check that sandbox and slice have the same job.\n if (this.jobAddress !== this.slice.jobAddress)\n throw new Error(`Sandbox.work: sandbox ${this.identifier} and slice ${this.slice.identifier} are from different jobsz`);\n\n /** @todo Should sliceHnd just be replaced with { sandbox: this } since this.public is part of this? */\n let sliceHnd = { job: this.public, sandbox: this };\n await this.resetState();\n if (!this.slice)\n {\n this.error(`Slice for job ${this.jobAddress} vanished during work initialization - aborting`);\n return;\n }\n\n const { datum: inputDatum, error: dataError } = this.slice;\n if (dataError)\n {\n that.postWorkEmit('error', {\n message: dataError.message,\n stack: dataError.stack,\n name: this.public.name\n });\n }\n\n this.resetProgressTimeout();\n this.resetSliceTimeout();\n\n return new Promise(function sandbox$$workPromise(resolve, reject) {\n function sandbox$$work$success (event)\n {\n that.removeListener('reject', sandbox$$work$fail);\n resolve(event);\n }\n\n function sandbox$$work$fail (error)\n {\n that.removeListener('resolve', sandbox$$work$success);\n reject(error);\n }\n\n that.once('resolve', sandbox$$work$success);\n that.once('reject', sandbox$$work$fail);\n\n that.sliceStartTime = Date.now();\n that.slice.startTime = that.sliceStartTime;\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 {\n that.removeListener('resolve', sandbox$$work$success);\n that.removeListener('reject', sandbox$$work$fail);\n dcp_timers.setTimeout(() => reject(dataError), 0)\n }\n else\n {\n // Do the work.\n const message = { request: 'main', data: inputDatum, };\n that.postMessageToEvaluator(message);\n }\n })\n .then(async function sandbox$$work$then(event) {\n // Tell supervisor sandbox slot is available.\n that.slice.markAsWorkDone();\n\n selectiveDebug2() && console.debug('Sandbox.sliceFinish', that.identifier, event?.timeReport);\n that.sandboxEmit('sliceEnd', that.slice?.sliceNumber)\n that.emit('complete', that.jobAddress);\n\n // Reset slice property.\n that.slice = null;\n\n // JobManager.runSliceOnSandbox will transition WORKDONE -> ASSIGNED\n return event;\n })\n .catch(async function sandbox$$work$catch(error) {\n selectiveDebug() && console.debug('Sandbox.work catch', that.identifier, error);\n // Tell supervisor sandbox slot is available.\n if (that.slice)\n that.slice.markAsWorkDone();\n // Current sandbox will not be reused.\n // Do not overwrite that.slice because it is needed in subsequent error reporting.\n\n if (error instanceof NoProgressError)\n {\n const payload = {\n name: that.public.name,\n message: error.message,\n timestamp: Date.now() - that.sliceStartTime,\n };\n that.postWorkEmit('error', payload);\n that.postWorkEmit('noProgress', { ...payload, progressReports: that.progressReports });\n }\n if (error.name === 'EWORKREJECT')\n that.handleRejectedWork(that.sliceTimeReport);\n\n // Otherwise sandbox will be terminated in Supervisor.handleSandboxWorkError\n debugging('sandbox') && debug(`Sandbox ${that.identifier} failed to execute slice`, error);\n\n throw error;\n });\n }\n\n resetProgressTimeout()\n {\n const that = this;\n\n if (this.progressTimeout)\n dcp_timers.clearTimeout(this.progressTimeout);\n\n this.progressTimeout = dcp_timers.setTimeout(function sandbox$ProgressTimeout() {\n if (that.options.ignoreNoProgress)\n return that.warning('ENOPROGRESS silenced by localExec: In a remote worker, this slice would be stopped for not calling progress frequently enough.');\n\n that.emit('reject', new NoProgressError(`No progress event was received in the last ${that.hive.progressTimeout / 1000} seconds.`));\n }, this.hive.progressTimeout * timeDilation);\n // Allow workers and localExec to exit.\n this.progressTimeout.unref();\n }\n\n resetSliceTimeout()\n {\n const that = this;\n\n if (this.sliceTimeout)\n dcp_timers.clearTimeout(this.sliceTimeout);\n\n this.sliceTimeout = dcp_timers.setTimeout(function sandbox$SliceTimeout() {\n if (Sandbox.debugWork)\n return that.warning('Sandbox.debugWork: Ignoring slice timeout');\n\n that.emit('reject', new SliceTooSlowError(`Slice took longer than ${that.hive.sliceTimeout / 1000} seconds.`));\n }, this.hive.sliceTimeout * timeDilation);\n // Allow workers and localExec to exit.\n this.sliceTimeout.unref();\n }\n\n /**\n * Send payload to the workEmit endpoint in the event router.\n * @param {string} eventName\n * @param {*} payload\n * @returns {Promise<*>}\n */\n postWorkEmit (eventName, payload)\n {\n // Need to check if the sandbox hasn't been assigned a slice yet.\n if (!this.slice)\n this.error('Sandbox not assigned a slice before sending workEmit message to scheduler', payload, `'workEmit' event originates from '${eventName}' event`);\n else\n {\n const slice = this.slice;\n // Authorization should always be valid.\n if (!slice.authorizationMessage)\n this.warning(`workEmit: missing authorization message for slice ${slice.identifier}`);\n else\n {\n const workEmitPayload = {\n eventName,\n payload,\n job: slice.jobAddress,\n slice: slice.sliceNumber,\n worker: this.supervisor.workerId,\n authorizationMessage : slice.authorizationMessage,\n };\n return this.supervisor.dcp4.safeWorkEmit(workEmitPayload, `Failed to send workEmit (${eventName}) payload for slice ${slice.identifier}`)\n .then((success) => {\n if (!success)\n this.warning(`Message sent to workEmit is unauthorized; not accepted '${eventName}'`);\n });\n }\n }\n }\n\n /**\n * Save rejected slice timeReport data in this.slice.rejectedTimeReport, then when needed in\n * Supervisor.recordResult, merge this.slice.rejectedTimeReport into this.slice.timeReport.\n * @param {{ total: number, CPU: number, webGL: number, webGPU: number }} timeReport\n */\n handleRejectedWork (timeReport)\n {\n selectiveDebug() && console.debug('handleRejectedWork', this.identifier);\n // If the slice already has rejectedTimeReport, add this timeReport to it.\n // If not, assign this timeReport to slices rejectedTimeReport property\n if (this.slice)\n {\n if (!this.slice.rejectedTimeReport)\n this.slice.rejectedTimeReport = timeReport;\n else\n {\n ['total', 'CPU', 'webGL', 'webGPU'].forEach((key) => {\n if (timeReport[key])\n this.slice.rejectedTimeReport[key] += timeReport[key];\n });\n }\n }\n }\n\n /**\n * Attach CGIO to result returned by a slice workFn.\n * @param {*} completeData - results\n */\n attachCGIOToResult (completeData)\n {\n if (!completeData)\n throw new Error('Slice result is not ready'); // Should never fire.\n if (completeData['timeReport'])\n throw new Error('Slice result already has timeReport'); // Should never fire.\n if (completeData['dataReport'])\n throw new Error('Slice result already has dataReport'); // Should never fire.\n if (this.listenerCount('resolve') > 0)\n {\n completeData['timeReport'] = this.sliceTimeReport;\n completeData['dataReport'] = {\n InDataSize: this.moduleInDataSize + this.jobManager.inputDataSize + this.slice.inputDataSize,\n OutDataSize: this.sliceOutDataSize,\n };\n this.emit('resolve', completeData);\n selectiveDebug() && console.debug('attachCGIOToResult', this.moduleInDataSize, this.jobManager.inputDataSize, this.slice.inputDataSize, completeData['dataReport'].InDataSize);\n }\n else\n {\n // If there is no internal listener for 'resolve', the slice was rejected\n // and we need to update this.slice.rejectedTimeReport appropriately.\n this.handleRejectedWork(this.sliceTimeReport);\n }\n // Clear time and data reports so we can catch mistaken writes.\n this.sliceTimeReport = null;\n this.sliceOutDataSize = 0;\n }\n\n // _Idx\n //\n // handleRing0Message, handleRing1Message, handleRing2Message, handleRing3Message\n //\n\n async handleRing0Message(data) // eslint-disable-line require-await\n {\n debugging('ring0') && debug('Ring0', this.identifier, data.request);\n\n switch (data.request)\n {\n case 'scriptLoaded':\n if(data.result !== \"success\")\n this.onerror(data);\n break;\n case 'error':\n debug('Sandbox error in ring0', data.error);\n this.rejectWithCleanup('during initialization', data.error);\n break;\n default:\n this.error('Received unhandled request from sandbox: ' + data.request, null, `data: ${ JSON.stringify(data)}`);\n break;\n }\n }\n\n async handleRing1Message(data) // eslint-disable-line require-await\n {\n debugging('ring1') && debug('Ring1', this.identifier, data.request);\n\n switch (data.request)\n {\n case 'applyRequirementsDone':\n // emit internally\n this.emit(data.request, data)\n break;\n default:\n this.error('Received unhandled request from sandbox ring 1: ' + data.request, null, `data: ${ JSON.stringify(data)}`);\n break; \n }\n }\n\n async handleRing2Message(data)\n {\n debugging('ring2') && debug('Ring2', this.identifier, data.request);\n\n switch (data.request)\n {\n case 'dependency': {\n try\n {\n const moduleData = await this.moduleCache.fetchModule(data.data, this.jobAddress);\n // Success! Restore this['packageManager'] delay to retryMinSleepMs (currently 32ms.)\n // Is there a better way to reset than explicit calls?\n this.supervisor.delayManager.resetEBO('packageManager');\n // Send module data to be evaluator.\n const message = {\n request: 'moduleGroup',\n data: moduleData,\n id: data.id,\n };\n // Module data is dynamic since it may only be required in a conditional branch.\n // Moreover, on a long job, the published module itself may be updated on the scheduler.\n const moduleLength = kvin.stringify(moduleData).length; /** @TODO - fix per DCP-3750 */\n this.moduleInDataSize += moduleLength;\n selectiveDebug() && console.debug('Sandbox.Ring2.fetchModule size', this.moduleInDataSize, moduleLength);\n this.postMessageToEvaluator(message);\n }\n catch (error)\n {\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. In principle we shouldn't need a valid sandbox.slice at sandbox.assign.\n * However, in the implementation of Sup2 there is precisely 1 callsite of sandbox.assign and\n * we do have an associated slice at this point. So we make the assumption that sandbox.slice\n * is valid here.\n */\n if (!this.slice) // Design assumption\n throw new Error(`Must have valid slice in sandbox before sandbox.assign is called: ${this.identifier}`);\n\n const payload = {\n name: error.name,\n message: error.message,\n timestamp: error.timestamp ? error.timestamp : new Date(),\n };\n\n this.postWorkEmit('error', payload);\n this.emit('reject', error);\n\n debugging() && console.debug(`Sandbox.Ring2: fetchModule failed ${this.identifier}`, payload, error, Date.now() - this.supervisor.lastTime);\n\n // Close packageManager to start the connection reconnect logic.\n // Should we do a retry loop with fetchModule too?\n this.supervisor.dcp4.resetConnection('packageManager');\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.\n */\n debug('Sandbox error in ring2', data.error);\n this.rejectWithCleanup('during assignment and dependency resolution', data.error);\n break;\n case 'describe':\n case 'evalResult':\n case 'resetStateDone':\n case 'assigned':\n this.emit(data.request, data); // emit internally\n break;\n case 'reject':\n this.emit('reject', data.error); // emit internally\n break;\n default:\n this.error(`Received unhandled request from sandbox ring 2. Data: ${JSON.stringify(data, null, 2)}`);\n break;\n }\n }\n\n async handleRing3Message(data) // eslint-disable-line require-await\n {\n debugging('ring3') && debug('Ring3', this.identifier, data.request);\n\n switch (data.request)\n {\n case 'complete':\n dcp_timers.clearTimeout(this.progressTimeout);\n dcp_timers.clearTimeout(this.sliceTimeout);\n this.progressTimeout = this.sliceTimeout = null;\n\n if (this.progress === null)\n {\n if (this.options.ignoreNoProgress)\n this.warning(\"ENOPROGRESS silenced by localExec: Progress was not called during this slice's execution, in a remote sandbox this would cause the slice to fail.\");\n else\n {\n // If a progress update was never received (progress === null) then reject\n this.emit('reject', new NoProgressError('Sandbox never emitted a progress event.'));\n this.handleRejectedWork(this.sliceTimeReport);\n break;\n }\n }\n \n this.progress = 100;\n this.sliceOutDataSize += kvin.stringify(data.result).length; /** @TODO - fix per DCP-3750 */\n this.attachCGIOToResult(data);\n break;\n case 'progress':\n {\n const { progress, indeterminate, throttledReports, value } = data;\n this.progress = progress;\n // cf. job-noProgress.js\n const progressReport = {\n deltaMs: 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 this.resetProgressTimeout();\n this.sandboxEmit('progress', indeterminate || progress < 0 || progress > 100 ? undefined : progress);\n break;\n }\n case 'noProgress':\n this.emit('reject', new NoProgressError(data.message));\n break;\n case 'console':\n data.payload.message = kvin.marshal(data.payload.message);\n this.sliceOutDataSize += JSON.stringify(data.payload.message).length; /** @TODO - fix per DCP-3750 */\n this.postWorkEmit('console', data.payload);\n break;\n case 'emitEvent': /* ad-hoc event from the sandbox (work.emit) */\n this.postWorkEmit('custom', data.payload);\n break;\n case 'measurement':\n this.updateTime(data);\n break;\n case 'sandboxError': /* the sandbox itself has an error condition */\n debug(`Ring3 received a 'sandboxError' event for sandbox ${this.identifier}`, data.error);\n this.emit('sandboxError', data.error);\n this.rejectWithCleanup('internal sandbox error while executing work function', data.error);\n break;\n case 'workError': /* the work function threw/rejected */\n debug(`Ring3 received a 'workError' event for sandbox ${this.identifier}`, data.error);\n this.postWorkEmit('error', data.error);\n const wrappedError = new UncaughtExceptionError(data.error);\n this.rejectWithCleanup('error while executing work function', wrappedError);\n break;\n default:\n this.error('Received unhandled request from sandbox ring 3: ' + data.request, null, `data: ${ JSON.stringify(data)}`);\n break; \n }\n }\n\n /**\n * Try to send the error back to the reject handler in Sandbox.work.\n * But if the reject handler is not available (s.b. rare) then cleanup, emit error and throw.\n * @param {string} message\n * @param {Error|string} error\n */\n rejectWithCleanup (message, error)\n {\n if (this.listenerCount('reject') > 0)\n this.emit('reject', error);\n else\n {\n this.terminate(false);\n this.error(`Sandbox ${this.identifier} ${message}`, error);\n throw error;\n }\n }\n\n // _Idx\n //\n // onmessage, onerror, terminate\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 * where the name of the event is event.data.request.\n *\n * @param {object} event - event received from the evaaluator sandbox\n * @returns {Promise<void>}\n */\n onmessage (event)\n {\n debugging('event') && debug('onmessage-event', event.data.ringSource);\n if (Sandbox.debugEvents)\n console.debug('sandbox - eventDebug:', { id: this.id, state: this.state.valueOf(), event: JSON.stringify(event) });\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 {\n /** @todo XXXpfr Beware of this happening often. */\n this.error(`Message sent directly from raw postMessage for event ${event}. Terminating worker...`);\n this.terminate(true);\n }\n else\n {\n const handler = this.ringMessageHandlers[ringLevel];\n if (handler)\n return handler.call(this, data.value);\n this.warning(`No handler defined for message from ring ${ringLevel} for event ${event}.`);\n }\n }\n\n /**\n * Error handler for the internal sandbox.\n * Emits error event that gets handled up in the Worker class.\n */\n onerror (event)\n {\n /** @todo XXXpfr Beware of onerror firing often. */\n this.error(`Sandbox.onerror 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 {\n if (this.slice)\n this.returnSlice();\n if (this.isTerminated)\n return;\n\n selectiveDebug() && console.debug(`Terminate sandbox ${this.identifier}`);\n\n this.state = new Synchronizer(TERMINATED, [ UNREADY, READYING, READY_FOR_ASSIGN, ASSIGNING, ASSIGNED, WORKING, TERMINATED ]);\n\n dcp_timers.clearTimeout(this.progressTimeout);\n dcp_timers.clearTimeout(this.sliceTimeout);\n this.progressTimeout = this.sliceTimeout = null;\n\n if (this.evaluatorHandle && typeof this.evaluatorHandle.terminate === 'function')\n {\n try\n {\n this.evaluatorHandle.terminate(immediate);\n }\n catch (e)\n {\n this.error(`Error terminating sandbox ${this.id}:`, e);\n }\n finally\n {\n this.evaluatorHandle = null;\n }\n }\n\n if (reject)\n this.emit('reject', new Error(`Sandbox ${this.identifier} was terminated.`));\n\n this.sandboxEmit('end');\n }\n\n // _Idx\n //\n // updateTime, resetSliceReport, sandboxEmit, error, warning\n //\n\n /**\n * ringNPostMessage can send a `measurement` request and update these\n * totals.\n */\n updateTime (measurementEvent)\n {\n ['total', 'CPU', 'webGL', 'webGPU'].forEach((key) => {\n if (measurementEvent[key])\n this.sliceTimeReport[key] += measurementEvent[key];\n });\n }\n\n /**\n * Start over sandbox work timers.\n */\n resetSliceReport ()\n {\n this.sliceTimeReport = {\n total: 0,\n CPU: 0,\n webGL: 0,\n webGPU: 0,\n };\n this.sliceOutDataSize = 0;\n }\n\n /**\n * Safe event emitter on sandboxHandle.\n * @param {string} event\n * @param {...any} args\n */\n sandboxEmit(event, ...args)\n {\n this.supervisor.safeEmit(this.sandboxHandle, event, ...args);\n }\n\n /**\n * Error feedback to user.\n * @param {string} message\n * @param {Array<Error>|Error|string} [coreError]\n * @param {string} [additionalInfo]\n * @param {boolean} [supressStack=false]\n */\n error (message, coreError, additionalInfo, supressStack = false)\n {\n this.supervisor.error(message, coreError, additionalInfo, supressStack);\n }\n\n /**\n * Warning feedback to user.\n * @param {string[]} messages\n */\n warning (...messages)\n {\n this.supervisor.warning(...messages);\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;\n\n\n//# sourceURL=webpack://dcp/./src/dcp-client/worker/supervisor2/sandbox2.js?");
|
|
4801
|
+
eval("/**\n * @file dcp/src/dcp-client/worker/supervisor2/sandbox2.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, { ...this.options.sandboxOptions });\n * await sandbox.start();\n * sandbox.slice = slice;\n * await sandbox.assign(jobManager);\n * return sandbox.work()\n * .then((result) => {\n * slice.collectResult(result, true);\n * sandbox.checkSandboxReUse();\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 * Initial states:\n * UNREADY\n *\n * Terminal states:\n * TERMINATED\n *\n * Valid transitions:\n * ( sandbox.start )\n * UNREADY -> READYING -> READY_FOR_ASSIGN\n * READYING -> TERMINATED\n * ( sandbox.assign )\n * READY_FOR_ASSIGN -> ASSIGNING -> ASSIGNED\n * ASSIGNING -> TERMINATED\n * ( sandbox.markAsWorking )\n * ASSIGEND -> WORKING\n * ( sandbox.work )\n * WORKING -> ASSIGNED\n * -> TERMINATED\n * ( sandbox.terminate )\n * any -> TERMINATED\n *\n * @author Matthew Palma, mpalma@kingsds.network\n * Ryan Rossiter, ryan@kingsds.network\n * Wes Garland, wes@distributive.network\n * Paul, paul@distributive.network\n * @date May 2019\n * May 2019\n * Decemeber 2020\n * June, Dec 2022, Jan-May 2023\n * @module sandbox\n * @copyright Copyright (c) 2018-2023, Distributive Corp. All Rights Reserved\n */\n// @ts-check\n\n\nconst debugging = (__webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope)('worker');\nconst dcp_timers = __webpack_require__(/*! dcp/common/dcp-timers */ \"./src/common/dcp-timers.js\");\nconst { assert, assertEq3 } = __webpack_require__(/*! dcp/common/dcp-assert */ \"./src/common/dcp-assert.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__(/*! events */ \"./node_modules/events/events.js\");\nconst kvin = __webpack_require__(/*! kvin */ \"./node_modules/kvin/kvin.js\");\nconst { Config } = __webpack_require__(/*! ./config */ \"./src/dcp-client/worker/supervisor2/config.js\");\nconst common = __webpack_require__(/*! ./common */ \"./src/dcp-client/worker/supervisor2/common.js\");\nconst { selectiveDebug, truncateAddress, timeDilation, selectiveDebug2 } = common;\nconst { stringify } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\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// 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 TERMINATED = 'TERMINATED' // Sandbox is terminated.\nconst EVAL_RESULT_PREFIX = 'evalResult::';\n\nclass SandboxError extends Error\n{\n /**\n * @param {string} errorCode\n * @param {string|Error} msg\n */\n constructor(errorCode, msg)\n {\n super((msg.constructor?.name === 'String') ? msg : msg['message']);\n /** @type {string} */\n this.errorCode = errorCode;\n if (msg.constructor?.name !== 'String')\n for (const prop of [ 'name', 'code', 'stack', 'lineNumber', 'columnNumber' ])\n if (msg[prop]) this[prop] = msg[prop];\n }\n}\nclass NoProgressError extends SandboxError { constructor(msg) { super('ENOPROGRESS', msg); } }\nclass SliceTooSlowError extends SandboxError { constructor(msg) { super('ESLICETOOSLOW', msg); } }\nclass UncaughtExceptionError extends SandboxError { constructor(msg) { super('EUNCAUGHT', msg); } }\n\n/** @typedef {import('./slice2').Slice} Slice */\n/** @typedef {import('./index').Supervisor} Supervisor */\n/** @typedef {import('./job-manager').JobManager} JobManager */\n/** @typedef {import('./module-cache').ModuleCache} ModuleCache */\n/** @typedef {import('dcp/utils/jsdoc-types').SandboxOptions} SandboxOptions */\n\n/**\n * Public event emitter.\n * https://gitlab.com/Distributed-Compute-Protocol/dcp-docs-wes/-/blob/wip/worker/worker-events.md\n */\nclass SandboxHandle extends EventEmitter\n{\n /** @type {{ id: number, public: { name: string, description: string, link: string }, jobManager: JobManager, slice: Slice }} */\n #info;\n\n /**\n * @constructor\n * @param {Sandbox} sandbox\n */\n constructor (sandbox)\n {\n super({ captureRejections: false });\n this.#info = sandbox.info;\n }\n /** @type {number} */\n get id () { return this.#info.id; }\n /** @type {{ name: string, description: string, link: string }} */\n get public () { return this.#info.public ?? { name: '<unassigned>', description: '', link: '' }; }\n /** @type {string} */\n get jobAddress () { return this.#info.jobManager?.address; }\n /** @type {number} */\n get sliceNumber () { return this.#info.slice?.sliceNumber ?? -1; }\n}\nexports.SandboxHandle = SandboxHandle;\n\n//\n// Index to functionality -- search for '_Idx' to toggle through the index.\n//\n// 1) class Sandbox\n// 2) checkSandboxReUse, postMessageToEvaluator, changeState,\n// and punctuatedTimer is expiremental for replacing hard-coded timeouts.\n// 3) start, describe, assign, applyRequirements, assignEvaluator\n// 4) eval, resetState, work, resetProgressTimeout, resetSliceTimeout\n// 5) handleRing0Message, handleRing1Message, handleRing2Message, handleRing3Message\n// 6) onmessage, onerror, terminate\n// 7) updateTime, resetSliceReport, sandboxEmit, error, warning\n//\n\n// _Idx\n//\n// class Sandbox\n//\n\nclass Sandbox extends EventEmitter\n{\n /**\n * A Sandbox (i.e. a worker sandbox) which executes distributed slices.\n *\n * @constructor\n * @param {Supervisor} supervisor\n * @param {SandboxOptions} options\n */\n constructor (supervisor, options)\n {\n super({ captureRejections: false });\n /** @type {Supervisor} */\n this.supervisor = supervisor;\n /** @type {ModuleCache} */\n this.moduleCache = supervisor.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 {Synchronizer} */\n this.state = new Synchronizer(UNREADY, [ UNREADY, READYING, READY_FOR_ASSIGN, ASSIGNING, ASSIGNED, WORKING, TERMINATED ]);\n\n /** @type {{ id: number, public: { name: string, description: string, link: string }, jobManager: JobManager, slice: Slice }} */\n this.info = {\n id: Sandbox.getNewId(),\n public: null,\n jobManager: null,\n slice: null,\n };\n\n /**\n * Event emitter containing info that describes the sandbox.\n * @type {SandboxHandle}\n */\n this.sandboxHandle = new SandboxHandle(this);\n\n /** Properties of type object. */\n this.evaluatorHandle = null;\n this.capabilities = null;\n this.progressTimeout = null;\n this.sliceTimeout = null;\n this.rejectionData = null;\n\n /** @type {number?} */\n this.progress = 100;\n /** @type {{ last: { deltaMs: number, value: any, throttledReports: number }, lastDeterministic: { deltaMs: number, progress: number, value: any, throttledReports: number } }} */\n this.progressReports = null; // cf. job-noProgress.js\n /** @type {object} */\n this.progressTimeout = null;\n /** @type {object} */\n this.sliceTimeout = null;\n\n /** @type {{ total: number, CPU: number, webGL: number, webGPU: number }} */\n this.sliceTimeReport = null;\n /** @type {number} */\n this.moduleInDataSize = 0; // Sandbox level input size; set during assign, never reset.\n /** @type {number} */\n this.sliceOutDataSize = 0; // Slice level output size; reset for every slice executed.\n\n /** @type {number?} */\n this.sliceStartTime = null;\n /** @type {number} */\n this.useCounter = 1; // Anticipating the initial use.\n /** @type {Config} */\n this.hive = new Config();\n\n ///** @type {((data: any) => Promise<void>)[]} */\n this.ringMessageHandlers = [\n this.handleRing0Message,\n this.handleRing1Message,\n this.handleRing2Message,\n this.handleRing3Message,\n ];\n\n this.resetSliceReport();\n }\n\n /** @type {number} */\n get id () { return this.info.id; }\n /** @type {{ name: string, description: string, link: string }} */\n get public () { return this.info.public; }\n /** @type {{ name: string, description: string, link: string }} */\n set public (data) { this.info.public = data; }\n /** @type {JobManager} */\n get jobManager () { return this.info.jobManager; }\n /** @type {string} */\n get jobAddress () { return this.jobManager?.address; }\n /** @type {Slice} */\n get slice () { return this.info.slice; }\n /** @type {Slice} */\n set slice (slice) { this.info.slice = slice; }\n /** @type {number} */\n get sliceNumber () { return this.slice ? this.slice.sliceNumber : -1; }\n /** @type {number} */\n get generalTimeout () { return 2 * this.hive.generalTimeout; }\n /** @type {number} */\n get punctuatedTimeout () { return this.hive.generalTimeout; }\n\n /**\n * Debug string that characterizes sandbox.\n * @type {string}\n */\n get identifier()\n {\n if (!this.jobAddress)\n return `${this.id}.${this.state}`;\n const address = truncateAddress(this.jobAddress);\n if (this.slice)\n return `${this.id}.${address}.${this.state}~${this.slice.sliceNumber}`;\n return `${this.id}.${address}.${this.state}`;\n }\n\n /** @returns {number} */\n static getNewId() { return Sandbox.idCounter++; }\n\n /** @type {boolean} */\n get isReadyForAssign () { return this.state.is(READY_FOR_ASSIGN); }\n /** @type {boolean} */\n get isAssigned () { return this.state.is(ASSIGNED); }\n /** @type {boolean} */\n get isWorking () { return this.state.is(WORKING); }\n /** @type {boolean} */\n get isTerminated () { return this.state.is(TERMINATED); }\n\n // _Idx\n //\n // checkSandboxReUse, postMessageToEvaluator, changeState,\n // punctuatedTimer is expiremental for replacing hard-coded timeouts.\n //\n\n /**\n * Mark WORKING sandbox as ASSIGNED in preparation for possible reuse.\n * Allow use of sandbox on a given job up to a limit of dcpConfig.supervisor.sandbox.maxSandboxUse .\n */\n checkSandboxReUse ()\n {\n selectiveDebug2() && console.debug(`Sandbox2.checkSandboxReUse: useCounter ${this.useCounter}, ${this.identifier}`);\n if (this.useCounter++ < this.hive.maxSandboxUse)\n {\n this.state.set(WORKING, ASSIGNED);\n this.sandboxEmit('ready');\n }\n else\n {\n this.terminate(false);\n common.removeElement(this.supervisor.sandboxInventory, this);\n }\n }\n\n /** Transitions: ASSIGNED --> WORKING. */\n markAsWorking ()\n {\n if (!this.isAssigned)\n throw new Error(`Sandbox ${this.identifier} is not ready to work`);\n this.state.set(ASSIGNED, WORKING);\n }\n \n /**\n * Safely post message to evaluator.\n * @param {object} message\n */\n postMessageToEvaluator (message)\n {\n if (this.isTerminated) // When evaluator goes down, all sandboxes are terminated.\n throw new Error(`postMessageToEvaluator: Sandbox ${this.identifier} has been terminated.`);\n return this.evaluatorHandle.postMessage(message);\n }\n \n /**\n * Safely change state.\n * @param {string} currentState\n * @param {string} nextState\n */\n changeState (currentState, nextState)\n {\n if (this.isTerminated) // When evaluator goes down, all sandboxes are terminated.\n throw new Error(`changeState: Sandbox ${this.identifier} has been terminated.`);\n this.state.set(currentState, nextState);\n }\n\n /** Upon fatal error return slice to scheduler. */\n returnSlice ()\n {\n selectiveDebug() && console.debug('Sandbox.returnSlice', this.identifier);\n return this.supervisor.returnSlice(this.slice, 'Sandbox.returnSlice');\n }\n\n /**\n * @callback cbFn\n * @returns {void}\n */\n\n /**\n * UNUSED.\n * Future work.\n * Replaces the timers in:\n * describe,\n * applyRequirements,\n * resetState,\n * The idea is to have a long timeout with a warning every\n * 6 seconds saying why it is waiting.\n * @param {cbFn} body\n * @param {string} waitMessage\n * @param {string} timerExpiredMessage\n * @returns {Promise<{ closeIntervalTimer: cbFn }>}\n */\n punctuatedTimer(body, waitMessage, timerExpiredMessage)\n {\n const that = this;\n return new Promise((resolve, reject) => {\n let intervalCounter = 0;\n let intervalHandle = null;\n function closeIntervalTimer()\n {\n if (intervalHandle !== null)\n dcp_timers.clearTimeout(intervalHandle);\n intervalHandle = null;\n }\n intervalHandle = dcp_timers.setInterval(() => {\n if (++intervalCounter > 12)\n {\n closeIntervalTimer();\n that.error(timerExpiredMessage);\n }\n that.warning(waitMessage);\n body();\n }, this.punctuatedTimeout)\n // Allow workers and localExec to exit.\n intervalHandle.unref();\n resolve({ closeIntervalTimer });\n });\n }\n\n // _Idx\n //\n // start, describe, assign, applyRequirements, assignEvaluator\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 * Sandbox.start will terminate the sandbox upon failure.\n * @todo maybe preload specific modules or let the cache pass in what modules to load?\n *\n * @returns {Promise<void>}\n * @throws on failure to ready\n */\n async start ()\n {\n debug('Sandbox.start begin');\n await this.supervisor.delayManager.nextDelay('sandboxStart');\n this.changeState(UNREADY, READYING);\n\n try\n {\n // RING 0\n this.evaluatorHandle = new this.options.SandboxConstructor({\n name: `DCP Sandbox #${this.id}`,\n });\n // Annoying! onerror terminates sandbox which can happen independent of whether the slice\n // is ok or not. Since we don't know, we have to return the slice when onerror is called\n // during sandbox.work .\n /** @todo XXXpfr Beware of onerror firing often. */\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 const data = (event.data.serialized)\n ? kvin.parse(event.data.message)\n : 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(kvin.marshal(message));\n }\n\n const that = this;\n this.evaluatorHandle.addEventListener('end', function sandbox$start$addEventListener() {\n selectiveDebug() && console.debug(\"END:Sandbox evaluatorHandle end-handler\", that.identifier, new Date());\n that.supervisor.evaluator.shuttingDown = true;\n that.terminate(true);\n });\n\n // Don't let an open sockets prevent clean worker exit.\n if (this.evaluatorHandle.unref)\n this.evaluatorHandle.unref();\n\n // Now in RING 1\n\n // Now in RING 2\n await this.describe();\n this.changeState(READYING, READY_FOR_ASSIGN);\n\n // Emit the 'sandbox' event on the worker event emitter.\n this.supervisor.safeEmit(this.supervisor.worker, 'sandbox', this.sandboxHandle);\n }\n catch (error)\n {\n if (this.isTerminated)\n debug(`Failed to start sandbox because it is already terminated: ${this.identifier}.\\n\\tMay be due to screensaver worker being down or evaluator was stopped.`);\n else\n {\n debug(`Failed to start sandbox ${this.identifier}.`, error.message); // FIX s.b. error\n this.terminate(false);\n }\n throw error;\n }\n }\n\n /**\n * Sends a post message to describe its capabilities.\n * Side effect: Sets the capabilities property of the current sandbox.\n *\n * @returns {Promise<any>} Resolves with the sandbox's capabilities.\n * Rejects with an error saying a response was not received.\n * @memberof Sandbox\n */\n describe ()\n {\n debugging('sandbox') && debug('Beginning to describe evaluator', this.identifier);\n const that = this;\n\n return new Promise(function sandbox$describePromise(resolve, reject) {\n let describeTimeout;\n\n if (that.isTerminated) // When evaluator goes down, all sandboxes are terminated.\n reject(new Error(`Sandbox ${that.identifier} has been terminated.`));\n\n if (that.evaluatorHandle === null)\n reject(new Error(`Evaluator has not been initialized: ${that.identifier}`));\n\n function sandbox$describe$success(data)\n {\n if (describeTimeout !== false)\n {\n dcp_timers.clearTimeout(describeTimeout);\n describeTimeout = false;\n\n const { capabilities } = data;\n if (typeof capabilities === 'undefined')\n reject(new Error(`Did not receive capabilities from describe response: ${that.identifier}`));\n that.capabilities = capabilities;\n\n debugging('sandbox') && debug('Evaluator has been described');\n resolve(capabilities);\n }\n }\n // Emitted by handleRing2Message.\n that.once('describe', sandbox$describe$success);\n\n describeTimeout = dcp_timers.setTimeout(function sandbox$describe$fail() {\n if (describeTimeout !== false)\n {\n describeTimeout = false;\n that.removeListener('describe', sandbox$describe$success);\n reject(new Error( `Describe message timed-out. No describe response was received from the describe command: ${that.identifier}`));\n }\n }, that.generalTimeout);\n // Allow workers and localExec to exit.\n describeTimeout.unref();\n\n const message = {\n request: 'describe',\n };\n that.postMessageToEvaluator(message);\n });\n }\n\n /**\n * This will assign the sandbox with a job, loading its sandbox code into the sandbox.\n * Sandbox.assign will not terminate the sandbox upon failure.\n * The sandbox will be terminated in JobManager.assignSandbox .\n * @param {JobManager} jobManager - The job manager that will be the owner of this sandbox.\n * @returns {Promise<Sandbox>}\n * @throws on initialization failure\n */\n async assign (jobManager)\n {\n if (!this.slice) // Design assumption.\n throw new Error(`Must have valid sandbox.slice before sandbox.assign is called: ${this.identifier}`);\n\n await this.supervisor.delayManager.nextDelay('sandboxAssign');\n debug('Sandbox.assign', this.identifier, Date.now() - this.supervisor.lastTime);\n\n try\n {\n this.changeState(READY_FOR_ASSIGN, ASSIGNING);\n this.info.jobManager = jobManager;\n this.job = this.jobManager.jobMessage;\n\n /* At this point, the worker has decided that this sandbox will be associated with a specific job. \n Therefore, we emit the SandboxHandle<job> event*/\n this.sandboxEmit('job', jobManager.jobHandle);\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 ${truncateAddress(this.jobAddress)}`,\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.applyRequirements(this.job.requirements);\n //const _t0 = Date.now();\n await this.assignEvaluator();\n //console.log('Finished Sandbox.assignEvaluator', Date.now() - _t0);\n this.changeState(ASSIGNING, ASSIGNED);\n this.sandboxEmit('ready');\n }\n catch (error)\n {\n if (this.isTerminated)\n debug(`Failed to assign sandbox ${this.identifier} to evaluator because it is already terminated.\\n\\tMay be due to screensaver worker being down or evaluator was stopped.`);\n else\n {\n debug(`Failed to assign sandbox ${this.identifier} to evaluator.`);\n this.terminate(false);\n }\n throw error;\n }\n\n return this;\n }\n\n /**\n * Passes the job's requirements object into the sandbox so that the global access lists can be updated accordingly.\n * E.g. disallow access to OffscreenCanvas without environment.offscreenCanvas=true present.\n * Must be called after @start.\n *\n * @returns {Promise<void>} - resolves on success, rejects otherwise\n */\n applyRequirements (requirements)\n {\n assert(typeof requirements === 'object');\n const that = this;\n\n return new Promise(function sandbox$applyRequirementsPromise(resolve, reject) {\n let requirementTimeout;\n\n function sandbox$applyRequirements$success()\n {\n if (requirementTimeout !== false)\n {\n dcp_timers.clearTimeout(requirementTimeout);\n requirementTimeout = false;\n resolve();\n }\n }\n // Emitted by handleRing1Message.\n that.once('applyRequirementsDone', sandbox$applyRequirements$success);\n\n requirementTimeout = dcp_timers.setTimeout(function sandbox$finishApplySandboxRequirements$fail() {\n if (requirementTimeout !== false)\n {\n requirementTimeout = false;\n that.removeListener('applyRequirementsDone', sandbox$applyRequirements$success);\n reject(new Error(`applyRequirements never received 'applyRequirementsDone' response from sandbox: ${that.identifier}`));\n }\n }, that.generalTimeout);\n // Allow workers and localExec to exit.\n requirementTimeout.unref();\n\n const message = {\n requirements,\n request: 'applyRequirements',\n };\n that.postMessageToEvaluator(message);\n });\n }\n\n /**\n * Assign job to the evaluator.\n * @returns {Promise<any>} - resolves on success, rejects otherwise\n */\n assignEvaluator ()\n {\n debugging('sandbox') && console.debug('Begin assigning job to evaluator', this.identifier);\n const that = this;\n\n return new Promise(function sandbox$$assignEvaluatorPromise(resolve, reject) {\n function sandbox$assignEvaluator$success(event)\n {\n that.removeListener('reject', sandbox$assignEvaluator$fail);\n debugging('sandbox') && debug('Job assigned to evaluator');\n resolve(event);\n }\n\n function sandbox$assignEvaluator$fail(error)\n {\n that.removeListener('assigned', sandbox$assignEvaluator$success);\n that.error(`assignEvaluator failed(${that.identifier}): evaluator may be out of memory or the screensaver may be down.`, error);\n selectiveDebug() && console.debug('assignEvaluator failed', that.identifier, error);\n if (that.slice) // Normally the slice hasn't been set yet.\n that.returnSlice();\n reject(error);\n }\n\n // Emitted by handleRing2Message.\n that.once('assigned', sandbox$assignEvaluator$success);\n that.once('reject', sandbox$assignEvaluator$fail);\n\n // Had to add useStrict -- not sure if anything else was missed.\n const jobMessage = {\n address: that.job.address,\n arguments: that.job.arguments,\n dependencies: that.job.dependencies,\n modulePath: that.job.modulePath,\n public: that.job.public,\n requireModules: that.job.requireModules,\n requirePath: that.job.requirePath,\n workFunction: that.job.workFunction,\n useStrict: that.job.useStrict,\n };\n\n const message = {\n request: 'assign',\n job: jobMessage,\n sandboxConfig: that.hive.sandboxConfig,\n };\n that.postMessageToEvaluator(message);\n });\n }\n\n // _Idx\n //\n // eval, resetState, work, resetProgressTimeout, resetSliceTimeout\n //\n\n /**\n * Evaluates a string inside the sandbox.\n * @todo XXXpfr -- I don't understand how this gets called?\n * There's an old comment saying: \"no longer working though?\"\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 * @returns {Promise<any>} - resolves with eval result on success, rejects otherwise\n */\n eval (code, filename)\n {\n const that = this;\n const msgId = nanoid();\n\n return new Promise(function sandbox$$eval$Promise(resolve, reject) {\n const eventId = EVAL_RESULT_PREFIX + msgId;\n\n function sandbox$eval$success(event)\n {\n that.removeListener('reject', sandbox$eval$fail);\n resolve(event);\n };\n\n function sandbox$eval$fail(error)\n {\n that.removeListener(eventId, sandbox$eval$success);\n reject(error);\n };\n\n that.once(eventId, sandbox$eval$success);\n that.once('reject', sandbox$eval$fail);\n\n const message = {\n request: 'eval',\n data: code,\n filename,\n msgId,\n };\n that.postMessageToEvaluator(message);\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<void>} - resolves with result on success, rejects otherwise\n */\n resetState ()\n {\n const that = this;\n assert(this.isWorking); // Design assumption.\n\n return new Promise(function sandbox$resetStatePromise(resolve, reject) {\n let resetStateTimeout;\n\n function sandbox$resetState$success ()\n {\n if (resetStateTimeout !== false)\n {\n dcp_timers.clearTimeout(resetStateTimeout);\n resetStateTimeout = false;\n resolve();\n }\n }\n that.once('resetStateDone', sandbox$resetState$success);\n\n resetStateTimeout = dcp_timers.setTimeout(function sandbox$resetState$fail() {\n if (resetStateTimeout !== false)\n {\n resetStateTimeout = false;\n that.removeListener('resetStateDone', sandbox$resetState$success);\n reject(new Error(`resetState never received resetStateDone event from sandbox: ${that.identifier}`));\n }\n }, that.generalTimeout);\n // Allow workers and localExec to exit.\n resetStateTimeout.unref();\n\n const message = {\n request: 'resetState',\n };\n that.postMessageToEvaluator(message);\n });\n }\n\n /**\n * Executes a slice received from the supervisor.\n * Must be called after this.start, this.assign and this.markAsWorking .\n * Sandbox.work will not terminate the sandbox upon failure.\n * The sandbox will be terminated in Supervisor.handleSandboxWorkError .\n * @returns {Promise<any>} - resolves with result on success, rejects otherwise\n */\n async work ()\n {\n const that = this;\n\n if (!this.slice) // Design assumption\n throw new Error(`Must have valid sandbox.slice before sandbox.assign is called: ${this.identifier}`);\n\n await this.supervisor.delayManager.nextDelay('sandboxWork');\n debug('Sandbox.work begin', this.identifier, Date.now() - this.supervisor.lastTime);\n\n if (this.isTerminated) // When evaluator goes down, all sandboxes are terminated.\n throw new Error(`Sandbox ${this.identifier} has been terminated.`);\n if (!this.isWorking)\n throw new Error(`Sandbox ${this.identifier} in Sandbox.work must be marked as working.`)\n\n // cf. DCP-1719,1720\n this.resetSliceReport();\n\n // Check that sandbox and slice have the same job.\n if (this.jobAddress !== this.slice.jobAddress)\n throw new Error(`Sandbox.work: sandbox ${this.identifier} and slice ${this.slice.identifier} are from different jobsz`);\n\n /** @todo Should sliceHnd just be replaced with { sandbox: this } since this.public is part of this? */\n let sliceHnd = { job: this.public, sandbox: this };\n await this.resetState();\n if (!this.slice)\n {\n this.error(`Slice for job ${this.jobAddress} vanished during work initialization - aborting`);\n return;\n }\n\n const { datum: inputDatum, error: dataError } = this.slice;\n if (dataError)\n {\n that.postWorkEmit('error', {\n message: dataError.message,\n stack: dataError.stack,\n name: this.public.name\n });\n }\n\n this.resetProgressTimeout();\n this.resetSliceTimeout();\n\n return new Promise(function sandbox$$workPromise(resolve, reject) {\n function sandbox$$work$success (event)\n {\n that.removeListener('reject', sandbox$$work$fail);\n resolve(event);\n }\n\n function sandbox$$work$fail (error)\n {\n that.removeListener('resolve', sandbox$$work$success);\n reject(error);\n }\n\n that.once('resolve', sandbox$$work$success);\n that.once('reject', sandbox$$work$fail);\n\n that.sliceStartTime = Date.now();\n that.slice.startTime = that.sliceStartTime;\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 {\n that.removeListener('resolve', sandbox$$work$success);\n that.removeListener('reject', sandbox$$work$fail);\n dcp_timers.setTimeout(() => reject(dataError), 0)\n }\n else\n {\n // Do the work.\n const message = { request: 'main', data: inputDatum, };\n that.postMessageToEvaluator(message);\n }\n })\n .then(async function sandbox$$work$then(event) {\n // Tell supervisor sandbox slot is available.\n that.slice.markAsWorkDone();\n\n selectiveDebug2() && console.debug('Sandbox.sliceFinish', that.identifier, event?.timeReport);\n that.sandboxEmit('sliceEnd', that.slice?.sliceNumber)\n that.emit('complete', that.jobAddress);\n\n // Reset slice property.\n that.slice = null;\n\n // JobManager.runSliceOnSandbox will transition WORKDONE -> ASSIGNED\n return event;\n })\n .catch(async function sandbox$$work$catch(error) {\n selectiveDebug() && console.debug('Sandbox.work catch', that.identifier, error);\n // Tell supervisor sandbox slot is available.\n if (that.slice)\n that.slice.markAsWorkDone();\n // Current sandbox will not be reused.\n // Do not overwrite that.slice because it is needed in subsequent error reporting.\n\n if (error instanceof NoProgressError)\n {\n const payload = {\n name: that.public.name,\n message: error.message,\n timestamp: Date.now() - that.sliceStartTime,\n };\n that.postWorkEmit('error', payload);\n that.postWorkEmit('noProgress', { ...payload, progressReports: that.progressReports });\n }\n if (error.name === 'EWORKREJECT')\n that.handleRejectedWork(that.sliceTimeReport);\n\n // Otherwise sandbox will be terminated in Supervisor.handleSandboxWorkError\n debugging('sandbox') && debug(`Sandbox ${that.identifier} failed to execute slice`, error);\n\n throw error;\n });\n }\n\n resetProgressTimeout()\n {\n const that = this;\n\n if (this.progressTimeout)\n dcp_timers.clearTimeout(this.progressTimeout);\n\n this.progressTimeout = dcp_timers.setTimeout(function sandbox$ProgressTimeout() {\n if (that.options.ignoreNoProgress)\n return that.warning('ENOPROGRESS silenced by localExec: In a remote worker, this slice would be stopped for not calling progress frequently enough.');\n\n that.emit('reject', new NoProgressError(`No progress event was received in the last ${that.hive.progressTimeout / 1000} seconds.`));\n }, this.hive.progressTimeout * timeDilation);\n // Allow workers and localExec to exit.\n this.progressTimeout.unref();\n }\n\n resetSliceTimeout()\n {\n const that = this;\n\n if (this.sliceTimeout)\n dcp_timers.clearTimeout(this.sliceTimeout);\n\n this.sliceTimeout = dcp_timers.setTimeout(function sandbox$SliceTimeout() {\n if (Sandbox.debugWork)\n return that.warning('Sandbox.debugWork: Ignoring slice timeout');\n\n that.emit('reject', new SliceTooSlowError(`Slice took longer than ${that.hive.sliceTimeout / 1000} seconds.`));\n }, this.hive.sliceTimeout * timeDilation);\n // Allow workers and localExec to exit.\n this.sliceTimeout.unref();\n }\n\n /**\n * Send payload to the workEmit endpoint in the event router.\n * @param {string} eventName\n * @param {*} payload\n * @returns {Promise<*>}\n */\n postWorkEmit (eventName, payload)\n {\n // Need to check if the sandbox hasn't been assigned a slice yet.\n if (!this.slice)\n this.error('Sandbox not assigned a slice before sending workEmit message to scheduler', payload, `'workEmit' event originates from '${eventName}' event`);\n else\n {\n const slice = this.slice;\n // Authorization should always be valid.\n if (!slice.authorizationMessage)\n this.warning(`workEmit: missing authorization message for slice ${slice.identifier}`);\n else\n {\n const workEmitPayload = {\n eventName,\n payload,\n job: slice.jobAddress,\n slice: slice.sliceNumber,\n worker: this.supervisor.workerId,\n authorizationMessage : slice.authorizationMessage,\n };\n return this.supervisor.dcp4.safeWorkEmit(workEmitPayload, `Failed to send workEmit (${eventName}) payload for slice ${slice.identifier}`)\n .then((success) => {\n if (!success)\n this.warning(`Message sent to workEmit is unauthorized; not accepted '${eventName}'`);\n });\n }\n }\n }\n\n /**\n * Save rejected slice timeReport data in this.slice.rejectedTimeReport, then when needed in\n * Supervisor.recordResult, merge this.slice.rejectedTimeReport into this.slice.timeReport.\n * @param {{ total: number, CPU: number, webGL: number, webGPU: number }} timeReport\n */\n handleRejectedWork (timeReport)\n {\n selectiveDebug() && console.debug('handleRejectedWork', this.identifier);\n // If the slice already has rejectedTimeReport, add this timeReport to it.\n // If not, assign this timeReport to slices rejectedTimeReport property\n if (this.slice)\n {\n if (!this.slice.rejectedTimeReport)\n this.slice.rejectedTimeReport = timeReport;\n else\n {\n ['total', 'CPU', 'webGL', 'webGPU'].forEach((key) => {\n if (timeReport[key])\n this.slice.rejectedTimeReport[key] += timeReport[key];\n });\n }\n }\n }\n\n /**\n * Attach CGIO to result returned by a slice workFn.\n * @param {*} completeData - results\n */\n attachCGIOToResult (completeData)\n {\n if (!completeData)\n throw new Error('Slice result is not ready'); // Should never fire.\n if (completeData['timeReport'])\n throw new Error('Slice result already has timeReport'); // Should never fire.\n if (completeData['dataReport'])\n throw new Error('Slice result already has dataReport'); // Should never fire.\n if (this.listenerCount('resolve') > 0)\n {\n completeData['timeReport'] = this.sliceTimeReport;\n completeData['dataReport'] = {\n InDataSize: this.moduleInDataSize + this.jobManager.inputDataSize + this.slice.inputDataSize,\n OutDataSize: this.sliceOutDataSize,\n };\n this.emit('resolve', completeData);\n selectiveDebug() && console.debug('attachCGIOToResult', this.moduleInDataSize, this.jobManager.inputDataSize, this.slice.inputDataSize, completeData['dataReport'].InDataSize);\n }\n else\n {\n // If there is no internal listener for 'resolve', the slice was rejected\n // and we need to update this.slice.rejectedTimeReport appropriately.\n this.handleRejectedWork(this.sliceTimeReport);\n }\n // Clear time and data reports so we can catch mistaken writes.\n this.sliceTimeReport = null;\n this.sliceOutDataSize = 0;\n }\n\n // _Idx\n //\n // handleRing0Message, handleRing1Message, handleRing2Message, handleRing3Message\n //\n\n async handleRing0Message(data) // eslint-disable-line require-await\n {\n debugging('ring0') && debug('Ring0', this.identifier, data.request);\n\n switch (data.request)\n {\n // The 'sandboxLoaded' event is used by dcp-native; do not remove.\n case 'sandboxLoaded': // SAVE\n break;\n case 'scriptLoaded':\n if(data.result !== \"success\")\n this.onerror(data);\n break;\n case 'error':\n debug('Sandbox error in ring0', data.error);\n this.rejectWithCleanup('during initialization', data.error);\n break;\n default:\n this.error('Received unhandled request from sandbox: ' + data.request, null, `data: ${ JSON.stringify(data)}`);\n break;\n }\n }\n\n async handleRing1Message(data) // eslint-disable-line require-await\n {\n debugging('ring1') && debug('Ring1', this.identifier, data.request);\n\n switch (data.request)\n {\n case 'applyRequirementsDone':\n // emit internally\n this.emit(data.request, data)\n break;\n default:\n this.error('Received unhandled request from sandbox ring 1: ' + data.request, null, `data: ${ JSON.stringify(data)}`);\n break; \n }\n }\n\n async handleRing2Message(data)\n {\n debugging('ring2') && debug('Ring2', this.identifier, data.request);\n\n switch (data.request)\n {\n case 'dependency': {\n try\n {\n const moduleData = await this.moduleCache.fetchModule(data.data, this.jobAddress);\n // Success! Restore this['packageManager'] delay to retryMinSleepMs (currently 32ms.)\n // Is there a better way to reset than explicit calls?\n this.supervisor.delayManager.resetEBO('packageManager');\n // Send module data to be evaluator.\n const message = {\n request: 'moduleGroup',\n data: moduleData,\n id: data.id,\n };\n // Module data is dynamic since it may only be required in a conditional branch.\n // Moreover, on a long job, the published module itself may be updated on the scheduler.\n const moduleLength = kvin.stringify(moduleData).length; /** @TODO - fix per DCP-3750 */\n this.moduleInDataSize += moduleLength;\n selectiveDebug() && console.debug('Sandbox.Ring2.fetchModule size', this.moduleInDataSize, moduleLength);\n this.postMessageToEvaluator(message);\n }\n catch (error)\n {\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. In principle we shouldn't need a valid sandbox.slice at sandbox.assign.\n * However, in the implementation of Sup2 there is precisely 1 callsite of sandbox.assign and\n * we do have an associated slice at this point. So we make the assumption that sandbox.slice\n * is valid here.\n */\n if (!this.slice) // Design assumption\n throw new Error(`Must have valid slice in sandbox before sandbox.assign is called: ${this.identifier}`);\n\n const payload = {\n name: error.name,\n message: error.message,\n timestamp: error.timestamp ? error.timestamp : new Date(),\n };\n\n this.postWorkEmit('error', payload);\n this.emit('reject', error);\n\n debugging() && console.debug(`Sandbox.Ring2: fetchModule failed ${this.identifier}`, payload, error, Date.now() - this.supervisor.lastTime);\n\n // Close packageManager to start the connection reconnect logic.\n // Should we do a retry loop with fetchModule too?\n this.supervisor.dcp4.resetConnection('packageManager');\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.\n */\n debug('Sandbox error in ring2', data.error);\n this.rejectWithCleanup('during assignment and dependency resolution', data.error);\n break;\n case 'describe':\n case 'evalResult':\n case 'resetStateDone':\n case 'assigned':\n this.emit(data.request, data); // emit internally\n break;\n case 'reject':\n this.emit('reject', data.error); // emit internally\n break;\n default:\n this.error(`Received unhandled request from sandbox ring 2. Data: ${JSON.stringify(data, null, 2)}`);\n break;\n }\n }\n\n async handleRing3Message(data) // eslint-disable-line require-await\n {\n debugging('ring3') && debug('Ring3', this.identifier, data.request);\n\n switch (data.request)\n {\n case 'complete':\n dcp_timers.clearTimeout(this.progressTimeout);\n dcp_timers.clearTimeout(this.sliceTimeout);\n this.progressTimeout = this.sliceTimeout = null;\n\n if (this.progress === null)\n {\n if (this.options.ignoreNoProgress)\n this.warning(\"ENOPROGRESS silenced by localExec: Progress was not called during this slice's execution, in a remote sandbox this would cause the slice to fail.\");\n else\n {\n // If a progress update was never received (progress === null) then reject\n this.emit('reject', new NoProgressError('Sandbox never emitted a progress event.'));\n this.handleRejectedWork(this.sliceTimeReport);\n break;\n }\n }\n \n this.progress = 100;\n this.sliceOutDataSize += kvin.stringify(data.result).length; /** @TODO - fix per DCP-3750 */\n this.attachCGIOToResult(data);\n break;\n case 'progress':\n {\n const { progress, indeterminate, throttledReports, value } = data;\n this.progress = progress;\n // cf. job-noProgress.js\n const progressReport = {\n deltaMs: 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 this.resetProgressTimeout();\n this.sandboxEmit('progress', indeterminate || progress < 0 || progress > 100 ? undefined : progress);\n break;\n }\n case 'noProgress':\n this.emit('reject', new NoProgressError(data.message));\n break;\n case 'console':\n data.payload.message = kvin.marshal(data.payload.message);\n this.sliceOutDataSize += JSON.stringify(data.payload.message).length; /** @TODO - fix per DCP-3750 */\n this.postWorkEmit('console', data.payload);\n break;\n case 'emitEvent': /* ad-hoc event from the sandbox (work.emit) */\n this.postWorkEmit('custom', data.payload);\n break;\n case 'measurement':\n this.updateTime(data);\n break;\n case 'sandboxError': /* the sandbox itself has an error condition */\n debug(`Ring3 received a 'sandboxError' event for sandbox ${this.identifier}`, data.error);\n this.emit('sandboxError', data.error);\n this.rejectWithCleanup('internal sandbox error while executing work function', data.error);\n break;\n case 'workError': /* the work function threw/rejected */\n debug(`Ring3 received a 'workError' event for sandbox ${this.identifier}`, data.error);\n this.postWorkEmit('error', data.error);\n const wrappedError = new UncaughtExceptionError(data.error);\n this.rejectWithCleanup('error while executing work function', wrappedError);\n break;\n default:\n this.error('Received unhandled request from sandbox ring 3: ' + data.request, null, `data: ${ JSON.stringify(data)}`);\n break; \n }\n }\n\n /**\n * Try to send the error back to the reject handler in Sandbox.work.\n * But if the reject handler is not available (s.b. rare) then cleanup, emit error and throw.\n * @param {string} message\n * @param {Error|string} error\n */\n rejectWithCleanup (message, error)\n {\n if (this.listenerCount('reject') > 0)\n this.emit('reject', error);\n else\n {\n this.terminate(false);\n this.error(`Sandbox ${this.identifier} ${message}`, error);\n throw error;\n }\n }\n\n // _Idx\n //\n // onmessage, onerror, terminate\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 * where the name of the event is event.data.request.\n *\n * @param {object} event - event received from the evaaluator sandbox\n * @returns {Promise<void>}\n */\n onmessage (event)\n {\n debugging('event') && debug('onmessage-event', event.data.ringSource);\n if (Sandbox.debugEvents)\n console.debug('sandbox - eventDebug:', { id: this.id, state: this.state.valueOf(), event: JSON.stringify(event) });\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 {\n /** @todo XXXpfr Beware of this happening often. */\n this.error(`Message sent directly from raw postMessage for event ${event}. Terminating worker...`);\n this.terminate(true);\n }\n else\n {\n const handler = this.ringMessageHandlers[ringLevel];\n if (handler)\n return handler.call(this, data.value);\n this.warning(`No handler defined for message from ring ${ringLevel} for event ${event}.`);\n }\n }\n\n /**\n * Error handler for the internal sandbox.\n * Emits error event that gets handled up in the Worker class.\n */\n onerror (event)\n {\n /** @todo XXXpfr Beware of onerror firing often. */\n this.error(`Sandbox.onerror 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 {\n if (this.slice)\n this.returnSlice();\n if (this.isTerminated)\n return;\n\n selectiveDebug() && console.debug(`Terminate sandbox ${this.identifier}`);\n\n this.state = new Synchronizer(TERMINATED, [ UNREADY, READYING, READY_FOR_ASSIGN, ASSIGNING, ASSIGNED, WORKING, TERMINATED ]);\n\n dcp_timers.clearTimeout(this.progressTimeout);\n dcp_timers.clearTimeout(this.sliceTimeout);\n this.progressTimeout = this.sliceTimeout = null;\n\n if (this.evaluatorHandle && typeof this.evaluatorHandle.terminate === 'function')\n {\n try\n {\n this.evaluatorHandle.terminate(immediate);\n }\n catch (e)\n {\n this.error(`Error terminating sandbox ${this.id}:`, e);\n }\n finally\n {\n this.evaluatorHandle = null;\n }\n }\n\n if (reject)\n this.emit('reject', new Error(`Sandbox ${this.identifier} was terminated.`));\n\n this.sandboxEmit('end');\n }\n\n // _Idx\n //\n // updateTime, resetSliceReport, sandboxEmit, error, warning\n //\n\n /**\n * ringNPostMessage can send a `measurement` request and update these\n * totals.\n */\n updateTime (measurementEvent)\n {\n ['total', 'CPU', 'webGL', 'webGPU'].forEach((key) => {\n if (measurementEvent[key])\n this.sliceTimeReport[key] += measurementEvent[key];\n });\n }\n\n /**\n * Start over sandbox work timers.\n */\n resetSliceReport ()\n {\n this.sliceTimeReport = {\n total: 0,\n CPU: 0,\n webGL: 0,\n webGPU: 0,\n };\n this.sliceOutDataSize = 0;\n }\n\n /**\n * Safe event emitter on sandboxHandle.\n * @param {string} event\n * @param {...any} args\n */\n sandboxEmit(event, ...args)\n {\n this.supervisor.safeEmit(this.sandboxHandle, event, ...args);\n }\n\n /**\n * Error feedback to user.\n * @param {string} message\n * @param {Array<Error>|Error|string} [coreError]\n * @param {string} [additionalInfo]\n * @param {boolean} [supressStack=false]\n */\n error (message, coreError, additionalInfo, supressStack = false)\n {\n this.supervisor.error(message, coreError, additionalInfo, supressStack);\n }\n\n /**\n * Warning feedback to user.\n * @param {string[]} messages\n */\n warning (...messages)\n {\n this.supervisor.warning(...messages);\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;\n\n\n//# sourceURL=webpack://dcp/./src/dcp-client/worker/supervisor2/sandbox2.js?");
|
|
4802
4802
|
|
|
4803
4803
|
/***/ }),
|
|
4804
4804
|
|
|
@@ -4809,7 +4809,7 @@ eval("/**\n * @file dcp/src/dcp-client/worker/supervisor2/sandbox2.js\n *\n * A
|
|
|
4809
4809
|
/***/ ((__unused_webpack_module, exports, __webpack_require__) => {
|
|
4810
4810
|
|
|
4811
4811
|
"use strict";
|
|
4812
|
-
eval("/**\n * @file dcp/src/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@distributive.network\n * Paul, paul@distributive.network\n * @date May 2019\n * May 2019\n * Decemeber 2020\n * June 2022, Feb-April 2023\n * @module slice\n * @copyright Copyright (c) 2018-2023, Distributive Corp. All Rights Reserved\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 { a$sleepMs, stringify } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\nconst { truncateAddress, selectiveDebug } = __webpack_require__(/*! ./common */ \"./src/dcp-client/worker/supervisor2/common.js\");\n\nconst INITIAL = 'INITIAL';\nconst READY = 'READY';\nconst RESERVED = 'RESERVED';\nconst WORKING = 'WORKING';\nconst WORKDONE = 'WORKDONE';\nconst COMPLETE = 'COMPLETE';\nconst FAILED = 'FAILED';\nconst FINISHED = 'FINISHED';\n\n/** @typedef {string} opaqueId */ // 22 character base64 string\n/** @typedef {import('./job-manager').JobHandle} JobHandle */\n/** @typedef {import('./job-manager').JobManager} JobManager */\n/** @typedef {import('dcp/utils/jsdoc-types').AuthMessage} AuthMessage */\n/** @typedef {import('dcp/utils/jsdoc-types').SliceMessage} SliceMessage */\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 code.\n * Please improve when possible.\n * \n * The read-only properties of this object are as follows:\n * - state INITIAL | READY | RESERVED | WORKING | WORKDONE | COMPLETE | FAILED | FINISHED\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 execution longer than (supervisor.options.targetTaskDuration || 300) seconds\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, CPU, webGL } 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 * - isUnassigned true, when in state INITIAL\n * - isReady true, when in state READY\n * - isReserved true, when in state RESERVED\n * - isWorking true, when in state WORKING\n * - isWorkDone true, when in state WORKDONE <-- work has completed\n * - isComplete true, when in state COMPLETE <-- work has completed successfully\n * - isFailed true, when in state FAILED <-- work has failed\n * - isFinished true, when in state FINISHED <-- Indicates result submission has succeeded.\n *\n * - isQueued true, when in state INITIAL, READY\n * - isActive true, when in state RESERVED, WORKING, WORKDONE, COMPLETE\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 * - startTime time when slice execution started\n * - hasBeenRejected when true, slice has been rejected at least once\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 *\n * INITIAL -> READY: - when slice data has been successfully fetched\n * INITIAL -> BROKEN: - when slice data has not been fetched successfully\n * READY -> RESERVED: - when a slice has been scheduled to run on a sandbox\n * RESERVED -> WORKING: - right before Sandbox2.work is called\n * WORKING -> WORKDONE: - right after Sandbox2.work completes successfully or not\n * WORKDONE -> COMPLETE: - when Slice2.collectResults is called and Sandbox2.work completed successfully\n * WORKDONE -> FAILED: - when Slice2.collectResults is called and Sandbox2.work did not completed successfully\n * COMPLETE -> FINISHED: - when submitting results succeeds\n * COMPLETE -> FAILED: - when submitting results fails\n */\nclass Slice\n{\n /** @type {Synchronizer} */\n #state;\n /** @type {JobManager} */\n #jobManager;\n /** @type {SliceMessage} */\n #sliceMessage;\n /** @type {Promise<void>} */\n #fetchSliceDataPromise;\n /** @type {AuthMessage} */\n #authorizationMessage;\n // private objects\n #datum;\n #result;\n #error;\n\n /**\n * @param {JobManager} jobManager\n * @param {SliceMessage} sliceMessage\n * @param {AuthMessage} authorizationMessage\n */\n constructor (jobManager, sliceMessage, authorizationMessage)\n {\n this.#state = new Synchronizer(INITIAL, [ INITIAL, READY, RESERVED, WORKING, WORKDONE, COMPLETE, FAILED, FINISHED ]);\n this.#jobManager = jobManager;\n this.#sliceMessage = { ...sliceMessage };\n this.#fetchSliceDataPromise = null;\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 {boolean} */\n this.hasBeenRejected = false;\n /** @type {{ total: number, CPU: number, webGL: number, webGPU: number }} */\n this.rejectedTimeReport = null;\n /** @type {number} */\n this.inputDataSize = 0;\n \n assert(this.jobAddress === String(this.#sliceMessage.jobAddress));\n if (!this.authorizationMessage)\n throw new Error(`Undefined authorization for slice ${this.identifier}.`);\n \n /** \n * Start loading slice datum in the background. Once these are loaded, slice state will\n * transition to READY and the slice will be ready to transition to WORKING.\n */\n this.#fetchSliceDataPromise = this.fetchSliceData();\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 {JobHandle} */\n get jobHandle () { return this.#jobManager.jobHandle; }\n /** @type {string} */\n get resultStorageType () { return this.#sliceMessage.resultStorageType; }\n /** @type {string} */\n get resultStorageDetails () { return this.#sliceMessage.resultStorageDetails; }\n /** @type {Promise<void>} */\n get fetchSliceDataPromise () { return this.#fetchSliceDataPromise; }\n /** @type {AuthMessage} */\n get authorizationMessage () { return this.#authorizationMessage; }\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\n /** @type {boolean} */\n get isUnassigned () { return this.state.is(INITIAL); }\n /** @type {boolean} */\n get isReady () { return this.state.is(READY); }\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 /** @type {boolean} */\n get isWorking () { return this.state.is(WORKING); }\n /** @type {boolean} */\n get isWorkDone () { return this.state.is(WORKDONE); }\n /** @type {boolean} */\n get isComplete () { return this.state.is(COMPLETE); }\n /** @type {boolean} */\n get isFailed () { return this.state.is(FAILED); }\n /** @type {boolean} */\n get isFinished () { return this.state.is(FINISHED); }\n /** @type {boolean} */\n get isQueued () { return this.isUnassigned || this.isReady; }\n /** @type {boolean} */\n get isActive () { return this.isReserved || this.isWorking || this.isWorkDone || this.isComplete; }\n /** @type {boolean} */\n get isQueuedOrActive () { return !(this.isFailed || this.isFinished); }\n\n /** @type {string} */\n get identifier () {\n return `${this.sliceNumber}.${truncateAddress(this.jobAddress)}.${this.state}`;\n }\n /**\n * A unique string characterizing a slice. Used in key-value patterns.\n * @type {string}\n **/\n get key () {\n return `${this.sliceNumber}.${this.jobAddress}`;\n }\n /** \n * timeReport accessor that optionally updates from this.rejectedTimeReport. \n * @type {{ total: number, CPU: number, webGL: number, webGPU: number }}\n **/\n get timeReport ()\n {\n if (!this.result)\n throw new Error('Slice result is not ready'); // Should never fire.\n if (!this.result.timeReport)\n throw new Error('Slice timeReport is not ready'); // Should never fire.\n if (this.rejectedTimeReport?.total > 0 && this.result.timeReport)\n {\n // Data collected from sandboxes that rejected this slice.\n ['total', 'CPU', 'webGL', 'webGPU'].forEach((key) => {\n if (this.rejectedTimeReport[key])\n this.result.timeReport[key] += this.rejectedTimeReport[key];\n });\n this.rejectedTimeReport = null;\n }\n return this.result.timeReport;\n }\n /**\n * dataReport accessor for the IO part of CGIO.\n * @type {{ InDataSize: number, OutDataSize: number }}\n **/\n get dataReport ()\n {\n if (!this.result)\n throw new Error('Slice result is not ready'); // Should never fire.\n if (!this.result.dataReport)\n throw new Error('Slice dataReport is not ready'); // Should never fire.\n return this.result.dataReport;\n }\n /**\n * Return the elapsed time to estimated slice completion time.\n * @type {number}\n **/\n get etaMs ()\n {\n /** @todo XXXpfr Verify the startTime logic is correct. */\n if (this.startTime === null) return 0; // slice is either COMPLETE, FINISHED or FAILED\n let etaMs = this.jobManager.emaSliceTime;\n if (this.startTime) etaMs -= (Date.now() - this.startTime);\n if (etaMs < 0) etaMs = 0;\n return etaMs;\n }\n\n /** Start slice over, regardless of what state it is in. */\n resetState ()\n {\n if (this.isQueued) return;\n this.#state = new Synchronizer(READY, [ INITIAL, READY, RESERVED, WORKING, WORKDONE, COMPLETE, FAILED, FINISHED ]);\n this.#error = null;\n this.#result = null;\n }\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 * @returns {Slice}\n */\n markAsReserved ()\n {\n this.state.set(READY, RESERVED);\n this.startTime = Date.now(); // A ready slice has a finite lifetime (e.g. 5 minutes.)\n return this;\n }\n\n /**\n * Sets the slice status to WORKING, called when the slice is getting ready to be handed to a worker.\n * @returns {boolean}\n */\n markAsWorking() { return this.state.setIf(RESERVED, WORKING); }\n\n /** Something went wrong, but reuse the slice. */\n unReserve () { this.state.set(RESERVED, READY); }\n\n /**\n * Sets the slice status to WORKDONE, called in sandbox.work when slice has finished executiion.\n * This needs to be set before the sandbox 'complete' event fires, or else supervisor.roundRobinSlices\n * may miss scheduling another slice on the empty sandbox slot.\n **/\n markAsWorkDone() { this.state.set(WORKING, WORKDONE); }\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 /** Sets the slice status to FINISHED, called when the slice is being returned to scheduler by Supervisor.repoMan. */\n repoMan() { this.state.set(READY, FINISHED); }\n\n /** Hide the result when returning slices. */\n killResult () { this.#result = null; }\n\n /**\n * Fetch slice data.\n * @returns {Promise<void>}\n */\n fetchSliceData()\n {\n return this.jobManager.fetchSliceData(this.datumUri, this)\n .then ((sliceData) => {\n debugging('slice') && console.debug(`Slice ${this.identifier} is transitioning to READY`);\n this.inputDataSize = sliceData.size;\n this.#datum = sliceData.data;\n this.state.set(INITIAL, READY);\n })\n .catch((error) => {\n selectiveDebug() && console.debug(`fetchSliceData failed for slice ${this.identifier}`, error);\n this.#error = error;\n this.state.set(INITIAL, FAILED);\n throw error;\n });\n }\n\n /**\n * Receives a result from the scheduler.\n * When success is true, it will set the result to this.#result.\n * When success is false, it will set the error to this.#error.\n * If this.#result is populated, it is possible to set the error, but setting the result will throw.\n * If this.#error is populated, then collectResult cannot be called w/o emitting an exception.\n *\n * @param {object|Error} result - The result from the worker sandbox or an error.\n * @param {boolean} [success=true] - True if result is considered successful, false if error occurred.\n */\n collectResult(result, success = true)\n {\n debugging('slice') && console.debug(`collectResult for slice ${this.identifier}`, success, this.error);\n\n this.startTime = null;\n if (success && this.result)\n throw new Error(`Slice ${this.identifier} already has a result.`);\n if (this.error)\n throw new Error(`Slice ${this.identifier} already has an error property.`);\n\n if (success)\n {\n this.state.set(WORKDONE, COMPLETE); // Work completed successfully.\n this.#result = result;\n }\n else\n {\n if (this.isComplete)\n this.state.set(COMPLETE, FAILED); // Failed to successfully submit results.\n else if (this.isWorkDone)\n this.state.set(WORKDONE, FAILED); // Work did not complete successfully.\n else\n this.state.set(WORKING, FAILED); // Work really did not complete successfully.\n this.#error = result;\n }\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 payload 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 payload 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 this.killResult();\n\n debugging('slice') && console.debug(`getReturnMessagePayload for slice ${this.identifier} and reason ${reason}:`, this.error);\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}\nexports.Slice = Slice;\n\n\n//# sourceURL=webpack://dcp/./src/dcp-client/worker/supervisor2/slice2.js?");
|
|
4812
|
+
eval("/**\n * @file dcp/src/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@distributive.network\n * Paul, paul@distributive.network\n * @date May 2019\n * May 2019\n * Decemeber 2020\n * June 2022, Feb-April 2023\n * @module slice\n * @copyright Copyright (c) 2018-2023, Distributive Corp. All Rights Reserved\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 { a$sleepMs, stringify } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\nconst { truncateAddress, selectiveDebug } = __webpack_require__(/*! ./common */ \"./src/dcp-client/worker/supervisor2/common.js\");\n\nconst INITIAL = 'INITIAL';\nconst READY = 'READY';\nconst RESERVED = 'RESERVED';\nconst WORKING = 'WORKING';\nconst WORKDONE = 'WORKDONE';\nconst COMPLETE = 'COMPLETE';\nconst FAILED = 'FAILED';\nconst FINISHED = 'FINISHED';\n\n/** @typedef {string} opaqueId */ // 22 character base64 string\n/** @typedef {import('./job-manager').JobHandle} JobHandle */\n/** @typedef {import('./job-manager').JobManager} JobManager */\n/** @typedef {import('dcp/utils/jsdoc-types').AuthMessage} AuthMessage */\n/** @typedef {import('dcp/utils/jsdoc-types').SliceMessage} SliceMessage */\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 code.\n * Please improve when possible.\n * \n * The read-only properties of this object are as follows:\n * - state INITIAL | READY | RESERVED | WORKING | WORKDONE | COMPLETE | FAILED | FINISHED\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 execution longer than (supervisor.options.targetTaskDuration || 300) seconds\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, CPU, webGL } 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 * - isUnassigned true, when in state INITIAL\n * - isReady true, when in state READY\n * - isReserved true, when in state RESERVED\n * - isWorking true, when in state WORKING\n * - isWorkDone true, when in state WORKDONE <-- work has completed\n * - isComplete true, when in state COMPLETE <-- work has completed successfully\n * - isFailed true, when in state FAILED <-- work has failed\n * - isFinished true, when in state FINISHED <-- Indicates result submission has succeeded.\n *\n * - isQueued true, when in state INITIAL, READY\n * - isActive true, when in state RESERVED, WORKING, WORKDONE, COMPLETE\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 * - startTime time when slice execution started\n * - hasBeenRejected when true, slice has been rejected at least once\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 *\n * INITIAL -> READY: - when slice data has been successfully fetched\n * INITIAL -> BROKEN: - when slice data has not been fetched successfully\n * READY -> RESERVED: - when a slice has been scheduled to run on a sandbox\n * RESERVED -> WORKING: - right before Sandbox2.work is called\n * WORKING -> WORKDONE: - right after Sandbox2.work completes successfully or not\n * WORKDONE -> COMPLETE: - when Slice2.collectResults is called and Sandbox2.work completed successfully\n * WORKDONE -> FAILED: - when Slice2.collectResults is called and Sandbox2.work did not completed successfully\n * COMPLETE -> FINISHED: - when submitting results succeeds\n * COMPLETE -> FAILED: - when submitting results fails\n */\nclass Slice\n{\n /** @type {Synchronizer} */\n #state;\n /** @type {JobManager} */\n #jobManager;\n /** @type {SliceMessage} */\n #sliceMessage;\n /** @type {Promise<void>} */\n #fetchSliceDataPromise;\n /** @type {AuthMessage} */\n #authorizationMessage;\n // private objects\n #datum;\n #result;\n #error;\n\n /**\n * @param {JobManager} jobManager\n * @param {SliceMessage} sliceMessage\n * @param {AuthMessage} authorizationMessage\n */\n constructor (jobManager, sliceMessage, authorizationMessage)\n {\n this.#state = new Synchronizer(INITIAL, [ INITIAL, READY, RESERVED, WORKING, WORKDONE, COMPLETE, FAILED, FINISHED ]);\n this.#jobManager = jobManager;\n this.#sliceMessage = { ...sliceMessage };\n this.#fetchSliceDataPromise = null;\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 {boolean} */\n this.hasBeenRejected = false;\n /** @type {{ total: number, CPU: number, webGL: number, webGPU: number }} */\n this.rejectedTimeReport = null;\n /** @type {number} */\n this.inputDataSize = 0;\n \n assert(this.jobAddress === String(this.#sliceMessage.jobAddress));\n if (!this.authorizationMessage)\n throw new Error(`Undefined authorization for slice ${this.identifier}.`);\n \n /** \n * Start loading slice datum in the background. Once these are loaded, slice state will\n * transition to READY and the slice will be ready to transition to WORKING.\n */\n this.#fetchSliceDataPromise = this.fetchSliceData();\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 {JobHandle} */\n get jobHandle () { return this.#jobManager.jobHandle; }\n /** @type {string} */\n get resultStorageType () { return this.#sliceMessage.resultStorageType; }\n /** @type {string} */\n get resultStorageDetails () { return this.#sliceMessage.resultStorageDetails; }\n /** @type {Promise<void>} */\n get fetchSliceDataPromise () { return this.#fetchSliceDataPromise; }\n /** @type {AuthMessage} */\n get authorizationMessage () { return this.#authorizationMessage; }\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\n /** @type {boolean} */\n get isUnassigned () { return this.state.is(INITIAL); }\n /** @type {boolean} */\n get isReady () { return this.state.is(READY); }\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 /** @type {boolean} */\n get isWorking () { return this.state.is(WORKING); }\n /** @type {boolean} */\n get isWorkDone () { return this.state.is(WORKDONE); }\n /** @type {boolean} */\n get isComplete () { return this.state.is(COMPLETE); }\n /** @type {boolean} */\n get isFailed () { return this.state.is(FAILED); }\n /** @type {boolean} */\n get isFinished () { return this.state.is(FINISHED); }\n /** @type {boolean} */\n get isQueued () { return this.isUnassigned || this.isReady; }\n /** @type {boolean} */\n get isActive () { return this.isReserved || this.isWorking || this.isWorkDone || this.isComplete; }\n /** @type {boolean} */\n get isQueuedOrActive () { return !(this.isFailed || this.isFinished); }\n\n /** @type {string} */\n get identifier () {\n return `${this.sliceNumber}.${truncateAddress(this.jobAddress)}.${this.state}`;\n }\n /**\n * A unique string characterizing a slice. Used in key-value patterns.\n * @type {string}\n **/\n get key () {\n return `${this.sliceNumber}.${this.jobAddress}`;\n }\n /** \n * timeReport accessor that optionally updates from this.rejectedTimeReport. \n * @type {{ total: number, CPU: number, webGL: number, webGPU: number }}\n **/\n get timeReport ()\n {\n if (!this.result)\n throw new Error('Slice result is not ready'); // Should never fire.\n if (!this.result.timeReport)\n throw new Error('Slice timeReport is not ready'); // Should never fire.\n if (this.rejectedTimeReport?.total > 0 && this.result.timeReport)\n {\n // Data collected from sandboxes that rejected this slice.\n ['total', 'CPU', 'webGL', 'webGPU'].forEach((key) => {\n if (this.rejectedTimeReport[key])\n this.result.timeReport[key] += this.rejectedTimeReport[key];\n });\n this.rejectedTimeReport = null;\n }\n return this.result.timeReport;\n }\n /**\n * dataReport accessor for the IO part of CGIO.\n * @type {{ InDataSize: number, OutDataSize: number }}\n **/\n get dataReport ()\n {\n if (!this.result)\n throw new Error('Slice result is not ready'); // Should never fire.\n if (!this.result.dataReport)\n throw new Error('Slice dataReport is not ready'); // Should never fire.\n return this.result.dataReport;\n }\n /**\n * Return the elapsed time to estimated slice completion time.\n * @type {number}\n **/\n get etaMs ()\n {\n /** @todo XXXpfr Verify the startTime logic is correct. */\n if (this.startTime === null) return 0; // slice is either COMPLETE, FINISHED or FAILED\n let etaMs = this.jobManager.emaSliceTime;\n if (this.startTime) etaMs -= (Date.now() - this.startTime);\n if (etaMs < 0) etaMs = 0;\n return etaMs;\n }\n\n /** Start slice over, regardless of what state it is in. */\n resetState ()\n {\n if (this.isQueued) return;\n this.#state = new Synchronizer(READY, [ INITIAL, READY, RESERVED, WORKING, WORKDONE, COMPLETE, FAILED, FINISHED ]);\n this.#error = null;\n this.#result = null;\n }\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 * @returns {Slice}\n */\n markAsReserved ()\n {\n this.state.set(READY, RESERVED);\n this.startTime = Date.now(); // A ready slice has a finite lifetime (e.g. 5 minutes.)\n return this;\n }\n\n /**\n * Sets the slice status to WORKING, called when the slice is getting ready to be handed to a worker.\n * @returns {boolean}\n */\n markAsWorking() { return this.state.set(RESERVED, WORKING); }\n\n /** Something went wrong, but reuse the slice. */\n unReserve () { this.state.set(RESERVED, READY); }\n\n /**\n * Sets the slice status to WORKDONE, called in sandbox.work when slice has finished executiion.\n * This needs to be set before the sandbox 'complete' event fires, or else supervisor.roundRobinSlices\n * may miss scheduling another slice on the empty sandbox slot.\n **/\n markAsWorkDone() { this.state.set(WORKING, WORKDONE); }\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 /** Sets the slice status to FINISHED, called when the slice is being returned to scheduler by Supervisor.repoMan. */\n repoMan() { this.state.set(READY, FINISHED); }\n\n /** Hide the result when returning slices. */\n killResult () { this.#result = null; }\n\n /**\n * Fetch slice data.\n * @returns {Promise<void>}\n */\n fetchSliceData()\n {\n return this.jobManager.fetchSliceData(this.datumUri, this)\n .then ((sliceData) => {\n debugging('slice') && console.debug(`Slice ${this.identifier} is transitioning to READY`);\n this.inputDataSize = sliceData.size;\n this.#datum = sliceData.data;\n this.state.set(INITIAL, READY);\n })\n .catch((error) => {\n selectiveDebug() && console.debug(`fetchSliceData failed for slice ${this.identifier}`, error);\n this.#error = error;\n this.state.set(INITIAL, FAILED);\n throw error;\n });\n }\n\n /**\n * Receives a result from the scheduler.\n * When success is true, it will set the result to this.#result.\n * When success is false, it will set the error to this.#error.\n * If this.#result is populated, it is possible to set the error, but setting the result will throw.\n * If this.#error is populated, then collectResult cannot be called w/o emitting an exception.\n *\n * @param {object|Error} result - The result from the worker sandbox or an error.\n * @param {boolean} [success=true] - True if result is considered successful, false if error occurred.\n */\n collectResult(result, success = true)\n {\n debugging('slice') && console.debug(`collectResult for slice ${this.identifier}`, success, this.error);\n\n this.startTime = null;\n if (success && this.result)\n throw new Error(`Slice ${this.identifier} already has a result.`);\n if (this.error)\n throw new Error(`Slice ${this.identifier} already has an error property.`);\n\n if (success)\n {\n this.state.set(WORKDONE, COMPLETE); // Work completed successfully.\n this.#result = result;\n }\n else\n {\n if (this.isComplete)\n this.state.set(COMPLETE, FAILED); // Failed to successfully submit results.\n else if (this.isWorkDone)\n this.state.set(WORKDONE, FAILED); // Work did not complete successfully.\n else\n this.state.set(WORKING, FAILED); // Work really did not complete successfully.\n this.#error = result;\n }\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 payload 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 payload 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 this.killResult();\n\n debugging('slice') && console.debug(`getReturnMessagePayload for slice ${this.identifier} and reason ${reason}:`, this.error);\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}\nexports.Slice = Slice;\n\n\n//# sourceURL=webpack://dcp/./src/dcp-client/worker/supervisor2/slice2.js?");
|
|
4813
4813
|
|
|
4814
4814
|
/***/ }),
|
|
4815
4815
|
|
|
@@ -4841,7 +4841,7 @@ eval("/**\n * @file events/event-subscriber.js\n * @author Ryan Rossiter
|
|
|
4841
4841
|
/***/ ((__unused_webpack_module, exports, __webpack_require__) => {
|
|
4842
4842
|
|
|
4843
4843
|
"use strict";
|
|
4844
|
-
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 * Create, in ExpressJS terminology, an \"app\" or \"router\" that is bound to the given server.\n *\n * @param {object} [httpServer] server to bind to\n * @constructor\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 || error.message));\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=false] 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=false] 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=false] 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 * Class to represent incoming http request\n * param {object} httpRequest NodeJS request from httpServer\n * @constructor\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 * Class to represent outgoing http response\n * @constructor\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} [_data]\n * @param {string} [_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} [status]\n * @param {string|URL} [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 || error.message));\n}\n\n\n//# sourceURL=webpack://dcp/./src/node-libs/pseudo-express.js?");
|
|
4844
|
+
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 * Create, in ExpressJS terminology, an \"app\" or \"router\" that is bound to the given server.\n *\n * @param {object} [httpServer] server to bind to\n * @constructor\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 || error.message));\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=false] 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=false] 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=false] 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 requestPE = new PseudoExpressRequest (httpRequest);\n const responsePE = new PseudoExpressResponse(httpResponse, requestPE);\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 requestPE.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 requestPE.query[key] = value && decodeURIComponent(value.replace(/\\+/g, ' '));\n }\n }\n descriptor.callback(requestPE, responsePE);\n }\n else if (rMethod === 'post')\n {\n const chunks = [];\n requestPE.on('data', chunk => chunks.push(chunk));\n requestPE.on('end', function pseudoExpress$postEnd() {\n requestPE.body = Buffer.concat(chunks);\n descriptor.callback(requestPE, responsePE);\n });\n }\n else\n {\n descriptor.callback(requestPE, responsePE);\n }\n }\n catch(error)\n {\n dumpError(requestPE, responsePE, error);\n }\n \n if (descriptor.hasOwnProperty('retval'))\n return descriptor.retval;\n}\n\n/**\n * Class to represent incoming http request\n * param {object} httpRequest NodeJS request from httpServer\n * @constructor\n */\nfunction PseudoExpressRequest(httpRequest)\n{\n Object.assign(this, httpRequest);\n // headers is non-enumerable, inherited in node 16+, so need to explicitly assign it.\n this.headers = httpRequest.headers;\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 * Class to represent outgoing http response\n * @constructor\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} [_data]\n * @param {string} [_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} [status]\n * @param {string|URL} [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(requestPE, responsePE, error)\n{\n responsePE.set('content-type', 'text/plain; charset=utf8');\n responsePE.set('cache-control', 'no-cache');\n responsePE.set('access-control-allow-origin', '*');\n responsePE.set('access-control-allow-headers', 'content-type');\n responsePE.statusCode = 500;\n responsePE.send(`500 Internal Server Error accessing ${requestPE.url} (${new Date()})\\n\\n` + error.stack);\n}\n\n\n//# sourceURL=webpack://dcp/./src/node-libs/pseudo-express.js?");
|
|
4845
4845
|
|
|
4846
4846
|
/***/ }),
|
|
4847
4847
|
|
|
@@ -5197,7 +5197,7 @@ eval("/**\n * @file utils/http.js Helper module for things rel
|
|
|
5197
5197
|
/***/ ((module, exports, __webpack_require__) => {
|
|
5198
5198
|
|
|
5199
5199
|
"use strict";
|
|
5200
|
-
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 * Paul, paul@distributive.network\n * @date Feb 2020\n * April 2023\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 * @module dcp/utils\n * @copyright Copyright (c) 2018-2023, Distributive Corp. All Rights Reserved\n */\n/* global dcpConfig */ // eslint-disable-line no-redeclare\n// @ts-check\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\nObject.assign(exports, __webpack_require__(/*! ./at-exit */ \"./src/utils/at-exit.js\"));\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__(/*! ./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 = __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(function __a$sleepMs() { 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|number} - 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').Instance)({level: exports.useChalk ? 1 : 0});\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 * Detect whether value is from the output of job-values:serializeJobValue.\n * @param {*} value\n * @returns {boolean}\n */\nexports.isFromSerializeJobValue = function utils$$isFromSerializeJobValue(value)\n{\n return value.hasOwnProperty('string') && value.hasOwnProperty('method') && value.hasOwnProperty('MIMEType') && Object.keys(value).length === 3;\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 utils$$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 = 2) {\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 if (str && truncationLength > 0)\n return str.slice(0, truncationLength);\n return str;\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, 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, digits);\n}\n\n/**\n * @param {object} jobMap\n * @param {number} [digits=-1]\n * @param {function} [functor]\n * @returns {string}\n */\nexports.compressJobMap = function utils$$compressJobMap(jobMap, digits = -1, functor) {\n if (functor)\n {\n jobMap = { ...jobMap };\n for (const job in jobMap)\n jobMap[job] = jobMap[job].map((x) => functor(x));\n }\n return exports.compressJobArray(Object.entries(jobMap), digits);\n}\n\n/**\n * @param {object[]} jobArray\n * @param {number} [digits=-1]\n * @returns {string}\n */\nexports.compressJobArray = function utils$$compressJobArray(jobArray, digits = -1) {\n let output = '';\n for (let k = 0; k < jobArray.length; k++) {\n output += exports.compressJobValue(jobArray[k], digits);\n }\n return output;\n}\n\n/**\n * @param {object|object[]} jobValue\n * @param {number} [digits=-1]\n * @returns {string}\n */\nexports.compressJobValue = function utils$$compressJobValue(jobValue, digits = -1) {\n if (jobValue.job && jobValue.slices)\n return exports.compressJobEntry(jobValue.job, jobValue.slices, digits);\n if (jobValue.length === 2)\n return exports.compressJobEntry(jobValue[0], jobValue[1], digits);\n return 'nada';\n}\n\n/**\n * @param {*} jobAddress\n * @param {Array<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) : value;\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>} numberArray\n * @returns {string}\n */\nexports.compressRange = function utils$$compressRange(numberArray) {\n assert(numberArray && Array.isArray(numberArray));\n numberArray = [...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 = [...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 * categorizeContentType\n * Analyze contentType (cf. https://www.w3.org/Protocols/rfc1341/4_Content-Type.html) and\n * determine whether it represents JSON, KVIN, binary or text.\n *********************************************************************************************/\n\nfunction normalizeCharset(charset)\n{\n const checkQuote = (c) => { return (c === \"'\" || c === '\"' || c === \"`\"); }\n if (checkQuote(charset[0]) && checkQuote(charset[charset.length - 1]))\n charset = charset.slice(1, charset.length - 1);\n // We don't diagnose 1-sided quotes -- these are malformed.\n return charset.toLowerCase();\n}\n\n/**\n * Transforms \"text/plain;charset=utf8\" to { type: \"text/plain\", topType: \"text\", charset: \"utf8\" }\n * @param {string} contentType - getResponseHeader('content-type')\n * @returns {{ type: string, topType: string, charset: string }}\n */\nfunction parseContentType(contentType)\n{\n if (!contentType)\n return { type: \"text/plain\", topType: \"text\", charset: \"utf8\" };\n // Eliminate any whitespace\n contentType = contentType.replace(/\\s/g, '');\n // Parse contentType into tokens.\n // https://www.w3.org/Protocols/rfc1341/4_Content-Type.html\n // Content-Type := type \"/\" subtype *[\";\" parameter]\n // parameter := attribute \"=\" value\n const ctArray = contentType.split(';');\n const type = ctArray[0];\n const topType = type.split('/')[0];\n let charset;\n if (ctArray.length > 1)\n {\n const paramArray = ctArray[1].split('=');\n if (paramArray.length > 1 && paramArray[0].toLowerCase() === 'charset')\n charset = normalizeCharset(paramArray[1]);\n } else if (topType === 'text')\n charset = 'us-ascii'; // https://www.w3.org/Protocols/rfc1341/7_1_Text.html\n\n return { type, topType, charset };\n}\n/**\n * Transforms:\n * \"text/plain;charset=utf8\" --> { isJson: false, isKVIN: false, isBinary: false, isText: true }\n * \"application/JSON\" --> { isJson: true, isKVIN: false, isBinary: false, isText: false }\n * \"application/x-kvin\" --> { isJson: false, isKVIN: true, isBinary: false, isText: false }\n * \"application/octet-stream\" --> { isJson: false, isKVIN: false, isBinary: true, isText: false }\n * \"application/javascript\" --> { isJson: false, isKVIN: false, isBinary: false, isText: false }\n * @param {string} contentType - getResponseHeader('content-type')\n * @param {string} [overrideType] - when overrideType === \"JSON\", return { isJSON: true, isKVIN: false, isBinary: false, isText: false };\n * @returns {{ isJSON: boolean, isKVIN: boolean, isBinary: boolean, isText: boolean }}\n */\nexports.categorizeContentType = function justFetch$categorizeContentType(contentType, overrideType)\n{\n if (overrideType === 'JSON')\n return { isJSON: true, isKVIN: false, isBinary: false, isText: false };\n\n const { type, topType, charset } = parseContentType(contentType);\n\n const isJSON = (type === 'application/json');\n const isKVIN = (type === 'application/x-kvin');\n const isJS = (type === 'application/javascript');\n const isText = (topType === 'text'\n && (charset === 'utf-8' || charset === 'utf8' // utf8\n || charset === 'us-ascii' || charset === 'ascii' // 7bit\n || charset === 'iso-8859-1' || charset === 'latin1')); // 8bit\n const isBinary = !(isJSON || isKVIN || isText || isJS);\n\n return { isJSON, isKVIN, isBinary, isText };\n}\n\n/**\n * shuffle\n * hashGeneration\n *********************************************************************************************/\n\n/**\n * Randomly shuffle array.\n * @param {Array<any>} array\n */\nexports.shuffle = function utils$$shuffle(array) {\n for (let i = array.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1));\n const temp = array[i];\n array[i] = array[j];\n array[j] = temp;\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 * Helpers for communicating jobPerfData stats with Supervisor, RS, TD.\n * collectMetrics\n * constructAlpha\n * constructAlphaFromDb\n * localTime\n * globalTime\n * globalTimeFromDb\n * totalTimeFromDb\n * nextEma\n *********************************************************************************************/\n\n/**\n * Construct the payload for updating job metrics in Supervisor.\n * Returned as part of getJobsForTask and in result submitter 'result' processing.\n * @param {*} job - Row from jobs table.\n * @param {*} jobPerfData - Correpsonding row from jobPerfData table.\n * @returns {{sliceCPUTime: number, sliceGPUTime: number, sliceCPUDensity: number, sliceGPUDensity: number, lastSliceNumber: number, measuredSlices: number, alpha: number}}\n */\nexports.collectMetrics = function utils$$collectMetrics(job, jobPerfData)\n{\n const lastSliceNumber = Number(job.lastSliceNumber || 0);\n const nextSliceNumber = Number(job.nextSliceNumber || 0);\n // Invariant jobPerfData.measuredSlices <= min(job.nextSliceNumber-1, job.lastSliceNumber)\n const measuredSlices = Math.min(Number(jobPerfData.measuredSlices), nextSliceNumber, lastSliceNumber);\n const alpha = exports.constructAlpha(lastSliceNumber, measuredSlices);\n const metrics = {\n sliceCPUTime: Number(jobPerfData.sliceCPUTime || 0),\n sliceGPUTime: Number(jobPerfData.sliceGPUTime || 0),\n sliceCPUDensity: Number(jobPerfData.sliceCPUDensity || 0),\n sliceGPUDensity: Number(jobPerfData.sliceGPUDensity || 0),\n sliceInDataSize: Number(jobPerfData.sliceInDataSize || 0),\n sliceOutDataSize: Number(jobPerfData.sliceOutDataSize || 0),\n // Transmit the EMA coefficient information to supervisor.\n lastSliceNumber,\n measuredSlices,\n alpha,\n };\n return metrics;\n}\n\n/**\n * Construct the EMA coefficient for computing jobPerfData columns.\n * @param {number} lastSliceNumber - job.lastSliceNumber\n * @param {number} measuredSlices - Math.min(jobPerfData.measuredSlices, job.nextSliceNumber, job.lastSliceNumber)\n * @returns {number} - 0 < retVal <= 0.5\n */\nexports.constructAlpha = function utils$$constructAlpha(lastSliceNumber, measuredSlices)\n{\n // lastSliceNumber - measuredSlices is # of slices which have not reported results.\n const alpha = Math.min(0.5, Math.max(1.0, lastSliceNumber - measuredSlices) / lastSliceNumber);\n return alpha;\n}\n\n/**\n * Construct the EMA coefficient for computing jobPerfData columns.\n * @param {*} job - row in jobs table\n * @param {*} jobPerfData - corresponding row in jobPerfData table\n * @returns {number} - 0 < retVal <= 0.5\n */\nexports.constructAlphaFromDb = function utils$$constructAlpha(job, jobPerfData)\n{\n const last = Number(job.lastSliceNumber || 0);\n const next = Number(job.nextSliceNumber || 0);\n const measured = Math.min(Number(jobPerfData.measuredSlices), next, last);\n const alpha = exports.constructAlpha(last, measured);\n return alpha;\n}\n\n/**\n * Compute density weighted time span.\n * @param {{CPUTime: number, GPUTime: number, CPUDensity: number, GPUDensity: number}} metrics\n * @returns {number}\n */\nexports.localTime = function utils$$localTime(metrics)\n{\n if (!metrics) return 0;\n const ltime = metrics.CPUTime + metrics.GPUTime;\n return ltime;\n}\n\n/**\n * Compute density weighted time span.\n * @param {{sliceCPUTime: number, sliceGPUTime: number, sliceCPUDensity: number, sliceGPUDensity: number}} metrics\n * @returns {number}\n */\nexports.globalTime = function utils$$globalTime(metrics)\n{\n if (!metrics) return 0;\n const gtime = metrics.sliceCPUTime + metrics.sliceGPUTime;\n return gtime;\n}\n\n/**\n * Compute total amount of time it takes to execute a slice.\n * @param {{sliceCPUTime: number, sliceGPUTime: number, sliceCPUDensity: number, sliceGPUDensity: number}} metrics\n * @returns {number}\n */\nexports.globalTotalTime = function utils$$globalTotalTime(metrics)\n{\n if (!metrics) return 0;\n const gttime = (metrics.sliceCPUTime + metrics.sliceGPUTime) / ((metrics.sliceCPUDensity + metrics.sliceGPUDensity) || 1);\n return gttime;\n}\n\n/**\n * Compute density.\n * @param {{sliceCPUTime: number, sliceGPUTime: number, sliceCPUDensity: number, sliceGPUDensity: number}} metrics\n * @returns {number}\n */\nexports.globalDensity = function utils$$globalDensity(metrics)\n{\n const gdens = metrics?.sliceCPUDensity || 1;\n return gdens;\n}\n\n/**\n * Compute GPU density.\n * @param {{sliceCPUTime: number, sliceGPUTime: number, sliceCPUDensity: number, sliceGPUDensity: number}} metrics\n * @returns {number}\n */\nexports.globalGPUDensity = function utils$$globalGPUDensity(metrics)\n{\n const ggdens = metrics?.sliceGPUDensity || 1;\n return ggdens;\n}\n\n/**\n * Compute density weighted time span for running phase in requestTask.\n * @param {*} jobPerfData\n * @returns {number}\n */\nexports.globalTimeFromDb = function utils$$globalTimeFromDb(jobPerfData)\n{\n const metrics = {\n sliceCPUTime: Number(jobPerfData.sliceCPUTime || 0),\n sliceGPUTime: Number(jobPerfData.sliceGPUTime || 0),\n sliceCPUDensity: Number(jobPerfData.sliceCPUDensity || 0),\n sliceGPUDensity: Number(jobPerfData.sliceGPUDensity || 0),\n };\n return exports.globalTime(metrics);\n}\n\n/**\n * Compute density weighted time span for estimation phase in requestTask.\n * @param {*} jobPerfData\n * @returns {number}\n */\nexports.totalTimeFromDb = function utils$$totalTimeFromDb(jobPerfData)\n{\n const metrics = {\n sliceCPUTime: Number(jobPerfData.totalCPUTime),\n sliceGPUTime: Number(jobPerfData.totalGPUTime),\n sliceCPUDensity: Number(jobPerfData.sliceCPUDensity || 0),\n sliceGPUDensity: Number(jobPerfData.sliceGPUDensity || 0),\n };\n return exports.globalTime(metrics);\n}\n\n/**\n * Create next step in an EMA time series of numeric values.\n * return alpha * current + (1 - alpha) * previous;\n * @param {number} previous\n * @param {number} current\n * @param {number} alpha\n * @returns {number}\n */\nexports.nextEma = function utils$$nextEma(previous, current, alpha)\n{\n if (!previous)\n return current;\n return alpha * current + (1 - alpha) * previous;\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\nexports.chalk = function utils$$chalk()\n{\n return new ((__webpack_require__(/*! chalk */ \"./node_modules/chalk/source/index.js\").Instance))({level: exports.useChalk ? 1 : 0});\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?");
|
|
5200
|
+
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 * Paul, paul@distributive.network\n * @date Feb 2020\n * April 2023\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 * @module dcp/utils\n * @copyright Copyright (c) 2018-2023, Distributive Corp. All Rights Reserved\n */\n/* global dcpConfig */ // eslint-disable-line no-redeclare\n// @ts-check\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\nObject.assign(exports, __webpack_require__(/*! ./at-exit */ \"./src/utils/at-exit.js\"));\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__(/*! ./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 = __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(function __a$sleepMs() { 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|number} - 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').Instance)({level: exports.useChalk ? 1 : 0});\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 * Detect whether value is from the output of job-values:serializeJobValue.\n * @param {*} value\n * @returns {boolean}\n */\nexports.isFromSerializeJobValue = function utils$$isFromSerializeJobValue(value)\n{\n return value.hasOwnProperty('string') && value.hasOwnProperty('method') && value.hasOwnProperty('MIMEType') && Object.keys(value).length === 3;\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 utils$$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 = 2) {\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 if (str && truncationLength > 0)\n return str.slice(0, truncationLength);\n return str;\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, 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, digits);\n}\n\n/**\n * @param {object} jobMap\n * @param {number} [digits=-1]\n * @param {function} [functor]\n * @returns {string}\n */\nexports.compressJobMap = function utils$$compressJobMap(jobMap, digits = -1, functor) {\n if (functor)\n {\n jobMap = { ...jobMap };\n for (const job in jobMap)\n jobMap[job] = jobMap[job].map((x) => functor(x));\n }\n return exports.compressJobArray(Object.entries(jobMap), digits);\n}\n\n/**\n * @param {object[]} jobArray\n * @param {number} [digits=-1]\n * @returns {string}\n */\nexports.compressJobArray = function utils$$compressJobArray(jobArray, digits = -1) {\n let output = '';\n for (let k = 0; k < jobArray.length; k++) {\n output += exports.compressJobValue(jobArray[k], digits);\n }\n return output;\n}\n\n/**\n * @param {object|object[]} jobValue\n * @param {number} [digits=-1]\n * @returns {string}\n */\nexports.compressJobValue = function utils$$compressJobValue(jobValue, digits = -1) {\n if (jobValue.job && jobValue.slices)\n return exports.compressJobEntry(jobValue.job, jobValue.slices, digits);\n if (jobValue.length === 2)\n return exports.compressJobEntry(jobValue[0], jobValue[1], digits);\n return 'nada';\n}\n\n/**\n * @param {*} jobAddress\n * @param {Array<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) : value;\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>} numberArray\n * @returns {string}\n */\nexports.compressRange = function utils$$compressRange(numberArray) {\n assert(numberArray && Array.isArray(numberArray));\n numberArray = [...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 = [...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 * categorizeContentType\n * Analyze contentType (cf. https://www.w3.org/Protocols/rfc1341/4_Content-Type.html) and\n * determine whether it represents JSON, KVIN, binary or text.\n *********************************************************************************************/\n\nfunction normalizeCharset(charset)\n{\n const checkQuote = (c) => { return (c === \"'\" || c === '\"' || c === \"`\"); }\n if (checkQuote(charset[0]) && checkQuote(charset[charset.length - 1]))\n charset = charset.slice(1, charset.length - 1);\n // We don't diagnose 1-sided quotes -- these are malformed.\n return charset.toLowerCase();\n}\n\n/**\n * Transforms \"text/plain;charset=utf8\" to { type: \"text/plain\", topType: \"text\", charset: \"utf8\" }\n * @param {string} contentType - getResponseHeader('content-type')\n * @returns {{ type: string, topType: string, charset: string }}\n */\nfunction parseContentType(contentType)\n{\n if (!contentType)\n return { type: \"text/plain\", topType: \"text\", charset: \"utf8\" };\n // Eliminate any whitespace\n contentType = contentType.replace(/\\s/g, '');\n // Parse contentType into tokens.\n // https://www.w3.org/Protocols/rfc1341/4_Content-Type.html\n // Content-Type := type \"/\" subtype *[\";\" parameter]\n // parameter := attribute \"=\" value\n const ctArray = contentType.split(';');\n const type = ctArray[0];\n const topType = type.split('/')[0];\n let charset;\n if (ctArray.length > 1)\n {\n const paramArray = ctArray[1].split('=');\n if (paramArray.length > 1 && paramArray[0].toLowerCase() === 'charset')\n charset = normalizeCharset(paramArray[1]);\n } else if (topType === 'text')\n charset = 'us-ascii'; // https://www.w3.org/Protocols/rfc1341/7_1_Text.html\n\n return { type, topType, charset };\n}\n/**\n * Transforms:\n * \"text/plain;charset=utf8\" --> { isJson: false, isKVIN: false, isBinary: false, isText: true }\n * \"application/JSON\" --> { isJson: true, isKVIN: false, isBinary: false, isText: false }\n * \"application/x-kvin\" --> { isJson: false, isKVIN: true, isBinary: false, isText: false }\n * \"application/octet-stream\" --> { isJson: false, isKVIN: false, isBinary: true, isText: false }\n * \"application/javascript\" --> { isJson: false, isKVIN: false, isBinary: false, isText: false }\n * @param {string} contentType - getResponseHeader('content-type')\n * @param {string} [overrideType] - when overrideType === \"JSON\", return { isJSON: true, isKVIN: false, isBinary: false, isText: false };\n * @returns {{ isJSON: boolean, isKVIN: boolean, isBinary: boolean, isText: boolean }}\n */\nexports.categorizeContentType = function justFetch$categorizeContentType(contentType, overrideType)\n{\n if (overrideType === 'JSON')\n return { isJSON: true, isKVIN: false, isBinary: false, isText: false };\n\n const { type, topType, charset } = parseContentType(contentType);\n\n const isJSON = (type === 'application/json');\n const isKVIN = (type === 'application/x-kvin');\n const isJS = (type === 'application/javascript');\n const isText = (topType === 'text'\n && (charset === 'utf-8' || charset === 'utf8' // utf8\n || charset === 'us-ascii' || charset === 'ascii' // 7bit\n || charset === 'iso-8859-1' || charset === 'latin1')); // 8bit\n const isBinary = !(isJSON || isKVIN || isText || isJS);\n\n return { isJSON, isKVIN, isBinary, isText };\n}\n\n/**\n * shuffle\n * hashGeneration\n *********************************************************************************************/\n\n/**\n * Randomly shuffle array.\n * @param {Array<any>} array\n */\nexports.shuffle = function utils$$shuffle(array) {\n for (let i = array.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1));\n const temp = array[i];\n array[i] = array[j];\n array[j] = temp;\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 * Helpers for communicating jobPerfData stats with Supervisor, RS, TD.\n * collectMetrics\n * constructAlpha\n * constructAlphaFromDb\n * localTime\n * globalTime\n * globalTimeFromDb\n * totalTimeFromDb\n * nextEma\n *********************************************************************************************/\n\n/**\n * Construct the payload for updating job metrics in Supervisor.\n * Returned as part of getJobsForTask and in result submitter 'result' processing.\n * @param {*} job - Row from jobs table.\n * @param {*} jobPerfData - Correpsonding row from jobPerfData table.\n * @returns {{sliceCPUTime: number, sliceGPUTime: number, sliceCPUDensity: number, sliceGPUDensity: number, lastSliceNumber: number, measuredSlices: number, alpha: number}}\n */\nexports.collectMetrics = function utils$$collectMetrics(job, jobPerfData)\n{\n const lastSliceNumber = Number(job.lastSliceNumber || 0);\n const nextSliceNumber = Number(job.nextSliceNumber || 0);\n // Invariant jobPerfData.measuredSlices <= min(job.nextSliceNumber-1, job.lastSliceNumber)\n const measuredSlices = Math.min(Number(jobPerfData.measuredSlices), nextSliceNumber, lastSliceNumber);\n const alpha = exports.constructAlpha(lastSliceNumber, measuredSlices);\n const metrics = {\n sliceCPUTime: Number(jobPerfData.sliceCPUTime || 0),\n sliceGPUTime: Number(jobPerfData.sliceGPUTime || 0),\n sliceCPUDensity: Number(jobPerfData.sliceCPUDensity || 0),\n sliceGPUDensity: Number(jobPerfData.sliceGPUDensity || 0),\n sliceInDataSize: Number(jobPerfData.sliceInDataSize || 0),\n sliceOutDataSize: Number(jobPerfData.sliceOutDataSize || 0),\n // Transmit the EMA coefficient information to supervisor.\n lastSliceNumber,\n measuredSlices,\n alpha,\n };\n return metrics;\n}\n\n/**\n * Construct the EMA coefficient for computing jobPerfData columns.\n * @param {number} lastSliceNumber - job.lastSliceNumber\n * @param {number} measuredSlices - Math.min(jobPerfData.measuredSlices, job.nextSliceNumber, job.lastSliceNumber)\n * @returns {number} - 0 < retVal <= 0.5\n */\nexports.constructAlpha = function utils$$constructAlpha(lastSliceNumber, measuredSlices)\n{\n // lastSliceNumber - measuredSlices is # of slices which have not reported results.\n const alpha = Math.min(0.5, Math.max(1.0, lastSliceNumber - measuredSlices) / lastSliceNumber);\n return alpha;\n}\n\n/**\n * Construct the EMA coefficient for computing jobPerfData columns.\n * @param {*} job - row in jobs table\n * @param {*} jobPerfData - corresponding row in jobPerfData table\n * @returns {number} - 0 < retVal <= 0.5\n */\nexports.constructAlphaFromDb = function utils$$constructAlpha(job, jobPerfData)\n{\n const last = Number(job.lastSliceNumber || 0);\n const next = Number(job.nextSliceNumber || 0);\n const measured = Math.min(Number(jobPerfData.measuredSlices), next, last);\n const alpha = exports.constructAlpha(last, measured);\n return alpha;\n}\n\n/**\n * Compute density weighted time span.\n * @param {{CPUTime: number, GPUTime: number, CPUDensity: number, GPUDensity: number}} metrics\n * @returns {number}\n */\nexports.localTime = function utils$$localTime(metrics)\n{\n if (!metrics) return 0;\n const ltime = metrics.CPUTime + metrics.GPUTime;\n return ltime;\n}\n\n/**\n * Compute density weighted time span.\n * @param {{sliceCPUTime: number, sliceGPUTime: number, sliceCPUDensity: number, sliceGPUDensity: number}} metrics\n * @returns {number}\n */\nexports.globalTime = function utils$$globalTime(metrics)\n{\n if (!metrics) return 0;\n const gtime = metrics.sliceCPUTime + metrics.sliceGPUTime;\n return gtime;\n}\n\n/**\n * Compute total amount of time it takes to execute a slice.\n * @param {{sliceCPUTime: number, sliceGPUTime: number, sliceCPUDensity: number, sliceGPUDensity: number}} metrics\n * @returns {number}\n */\nexports.globalTotalTime = function utils$$globalTotalTime(metrics)\n{\n if (!metrics) return 0;\n const gttime = (metrics.sliceCPUTime + metrics.sliceGPUTime) / ((metrics.sliceCPUDensity + metrics.sliceGPUDensity) || 1);\n return gttime;\n}\n\n/**\n * Compute density.\n * @param {{sliceCPUTime: number, sliceGPUTime: number, sliceCPUDensity: number, sliceGPUDensity: number}} metrics\n * @returns {number}\n */\nexports.globalDensity = function utils$$globalDensity(metrics)\n{\n const gdens = metrics?.sliceCPUDensity || 1;\n return gdens;\n}\n\n/**\n * Compute GPU density.\n * @param {{sliceCPUTime: number, sliceGPUTime: number, sliceCPUDensity: number, sliceGPUDensity: number}} metrics\n * @returns {number}\n */\nexports.globalGPUDensity = function utils$$globalGPUDensity(metrics)\n{\n const ggdens = metrics?.sliceGPUDensity || 1;\n return ggdens;\n}\n\n/**\n * Compute density weighted time span for running phase in requestTask.\n * @param {*} jobPerfData\n * @returns {number}\n */\nexports.globalTimeFromDb = function utils$$globalTimeFromDb(jobPerfData)\n{\n const metrics = {\n sliceCPUTime: Number(jobPerfData.sliceCPUTime || 0),\n sliceGPUTime: Number(jobPerfData.sliceGPUTime || 0),\n sliceCPUDensity: Number(jobPerfData.sliceCPUDensity || 0),\n sliceGPUDensity: Number(jobPerfData.sliceGPUDensity || 0),\n };\n return exports.globalTime(metrics);\n}\n\n/**\n * Compute density weighted time span for estimation phase in requestTask.\n * @param {*} jobPerfData\n * @returns {number}\n */\nexports.totalTimeFromDb = function utils$$totalTimeFromDb(jobPerfData)\n{\n const metrics = {\n sliceCPUTime: Number(jobPerfData.totalCPUTime),\n sliceGPUTime: Number(jobPerfData.totalGPUTime),\n sliceCPUDensity: Number(jobPerfData.sliceCPUDensity || 0),\n sliceGPUDensity: Number(jobPerfData.sliceGPUDensity || 0),\n };\n return exports.globalTime(metrics);\n}\n\n/**\n * Create next step in an EMA time series of numeric values.\n * return alpha * current + (1 - alpha) * previous;\n * @param {number} previous\n * @param {number} current\n * @param {number} alpha\n * @returns {number}\n */\nexports.nextEma = function utils$$nextEma(previous, current, alpha)\n{\n if (!previous)\n return current;\n return alpha * current + (1 - alpha) * previous;\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\nexports.chalk = function utils$$chalk()\n{\n return new ((__webpack_require__(/*! chalk */ \"./node_modules/chalk/source/index.js\").Instance))({level: exports.useChalk ? 1 : 0});\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?");
|
|
5201
5201
|
|
|
5202
5202
|
/***/ }),
|
|
5203
5203
|
|
|
@@ -5239,7 +5239,7 @@ eval("/* provided dependency */ var Buffer = __webpack_require__(/*! ./node_modu
|
|
|
5239
5239
|
\********************************/
|
|
5240
5240
|
/***/ ((__unused_webpack_module, exports) => {
|
|
5241
5241
|
|
|
5242
|
-
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}}
|
|
5242
|
+
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:1},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?");
|
|
5243
5243
|
|
|
5244
5244
|
/***/ }),
|
|
5245
5245
|
|
|
@@ -5292,7 +5292,7 @@ eval("/* provided dependency */ var process = __webpack_require__(/*! ./node_mod
|
|
|
5292
5292
|
/***/ ((__unused_webpack_module, exports, __webpack_require__) => {
|
|
5293
5293
|
|
|
5294
5294
|
"use strict";
|
|
5295
|
-
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\");\nconst { atExit, atExitCancel } = __webpack_require__(/*! ./index */ \"./src/utils/index.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, which contains removeFn(), a function which can remove all\n * traces of the temporary thing.\n *\n */\nexports.createTempThing = function createTempThing(createThingFn, basenamePattern, extension, tempDir)\n{\n const generateEntropy = (__webpack_require__(/*! nanoid */ \"./node_modules/nanoid/index.browser.js\").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 (__webpack_require__(/*! dcp/node-libs/shutdown */ \"./src/node-libs/shutdown.js\").atExitCancel)(removeFn);\n if (removed)\n return;\n if (!process.env.DCP_DEBUG_TMP_FILES)\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 throw error;\n fs.rmdirSync(filename, { recursive: true, maxRetries: 20 }); /* maxRetries might need rimraf? rtfm later /wg aug 2022 */\n removed = true;\n }\n }\n catch(error)\n {\n if (error.code !== 'ENOENT')\n process.emitWarning(`Warning: unable to clean up '${filename}'`, error.message);\n else\n removed = true;\n }\n }\n }\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 * @param {string} tempDir [optional] The directory in which to create the file. Default\n * is exports.systemTempDir.\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, tempDir)\n{\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, tempDir);\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 (__webpack_require__(/*! dcp/node-libs/shutdown */ \"./src/node-libs/shutdown.js\").atExit)(removeFn);\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 (__webpack_require__(/*! dcp/node-libs/shutdown */ \"./src/node-libs/shutdown.js\").atExit)(removeFn);\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 if (os.platform() !== 'win32') /* Pipe namespace automatically cleaned up on win32 */\n {\n socket.on('close', removeFn); \n (__webpack_require__(/*! dcp/node-libs/shutdown */ \"./src/node-libs/shutdown.js\").atExit)(removeFn);\n }\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?");
|
|
5295
|
+
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\");\nconst { atExit, atExitCancel } = __webpack_require__(/*! ./index */ \"./src/utils/index.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, which contains removeFn(), a function which can remove all\n * traces of the temporary thing.\n *\n */\nexports.createTempThing = function createTempThing(createThingFn, basenamePattern, extension, tempDir)\n{\n const generateEntropy = (__webpack_require__(/*! nanoid */ \"./node_modules/nanoid/index.browser.js\").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 (__webpack_require__(/*! dcp/node-libs/shutdown */ \"./src/node-libs/shutdown.js\").atExitCancel)(removeFn);\n if (removed)\n return;\n if (!process.env.DCP_DEBUG_TMP_FILES)\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 throw error;\n fs.rmdirSync(filename, { recursive: true, maxRetries: 20 }); /* maxRetries might need rimraf? rtfm later /wg aug 2022 */\n removed = true;\n }\n }\n catch(error)\n {\n if (error.code !== 'ENOENT')\n process.emitWarning(`Warning: unable to clean up '${filename}'`, error.message);\n else\n removed = true;\n }\n }\n }\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 * @param {string} tempDir [optional] The directory in which to create the file. Default\n * is exports.systemTempDir.\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, tempDir)\n{\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, tempDir);\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 (__webpack_require__(/*! dcp/node-libs/shutdown */ \"./src/node-libs/shutdown.js\").atExit)(removeFn);\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 (__webpack_require__(/*! dcp/node-libs/shutdown */ \"./src/node-libs/shutdown.js\").atExit)(removeFn);\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 if (os.platform() !== 'win32') /* Pipe namespace automatically cleaned up on win32 */\n {\n socket.on('close', removeFn); \n (__webpack_require__(/*! dcp/node-libs/shutdown */ \"./src/node-libs/shutdown.js\").atExit)(removeFn);\n }\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.slice(0, nRead));\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?");
|
|
5296
5296
|
|
|
5297
5297
|
/***/ }),
|
|
5298
5298
|
|