dcp-client 4.1.12 → 4.1.13

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.
@@ -96,7 +96,7 @@
96
96
  /*! exports provided: dcp_root, generated, build, install, default */
97
97
  /***/ (function(module) {
98
98
 
99
- eval("module.exports = JSON.parse(\"{\\\"dcp_root\\\":\\\"/home/erose/git/release/dcp/dist\\\",\\\"generated\\\":\\\"Fri 06 Aug 2021 11:03:07 AM EDT by erose on lorge\\\",\\\"build\\\":\\\"release\\\",\\\"install\\\":{\\\"generated\\\":\\\"Fri 06 Aug 2021 11:03:07 AM EDT by erose on lorge\\\",\\\"prefix\\\":\\\"/home/erose/git/release/dcp/dist\\\",\\\"build\\\":\\\"release\\\",\\\"node\\\":\\\"/usr/bin/node\\\",\\\"defaultUser\\\":\\\"erose\\\",\\\"defaultGroup\\\":\\\"erose\\\"}}\");\n\n//# sourceURL=webpack:///./etc/local-config.json?");
99
+ eval("module.exports = JSON.parse(\"{\\\"dcp_root\\\":\\\"/home/erose/git/release/dcp/dist\\\",\\\"generated\\\":\\\"Fri 22 Oct 2021 04:01:16 PM EDT by erose on dione\\\",\\\"build\\\":\\\"release\\\",\\\"install\\\":{\\\"generated\\\":\\\"Fri 22 Oct 2021 04:01:16 PM EDT by erose on dione\\\",\\\"prefix\\\":\\\"/home/erose/git/release/dcp/dist\\\",\\\"build\\\":\\\"release\\\",\\\"node\\\":\\\"/usr/bin/node\\\",\\\"defaultUser\\\":\\\"erose\\\",\\\"defaultGroup\\\":\\\"erose\\\"}}\");\n\n//# sourceURL=webpack:///./etc/local-config.json?");
100
100
 
101
101
  /***/ }),
102
102
 
@@ -2030,10 +2030,10 @@ eval("/* WEBPACK VAR INJECTION */(function(module) {(function (module, exports)
2030
2030
  /*!********************************************!*\
2031
2031
  !*** ./node_modules/elliptic/package.json ***!
2032
2032
  \********************************************/
2033
- /*! exports provided: name, version, description, main, files, scripts, repository, keywords, author, license, bugs, homepage, devDependencies, dependencies, _resolved, _integrity, _from, default */
2033
+ /*! exports provided: _args, _from, _id, _inBundle, _integrity, _location, _phantomChildren, _requested, _requiredBy, _resolved, _spec, _where, author, bugs, dependencies, description, devDependencies, files, homepage, keywords, license, main, name, repository, scripts, version, default */
2034
2034
  /***/ (function(module) {
2035
2035
 
2036
- eval("module.exports = JSON.parse(\"{\\\"name\\\":\\\"elliptic\\\",\\\"version\\\":\\\"6.5.4\\\",\\\"description\\\":\\\"EC cryptography\\\",\\\"main\\\":\\\"lib/elliptic.js\\\",\\\"files\\\":[\\\"lib\\\"],\\\"scripts\\\":{\\\"lint\\\":\\\"eslint lib test\\\",\\\"lint:fix\\\":\\\"npm run lint -- --fix\\\",\\\"unit\\\":\\\"istanbul test _mocha --reporter=spec test/index.js\\\",\\\"test\\\":\\\"npm run lint && npm run unit\\\",\\\"version\\\":\\\"grunt dist && git add dist/\\\"},\\\"repository\\\":{\\\"type\\\":\\\"git\\\",\\\"url\\\":\\\"git@github.com:indutny/elliptic\\\"},\\\"keywords\\\":[\\\"EC\\\",\\\"Elliptic\\\",\\\"curve\\\",\\\"Cryptography\\\"],\\\"author\\\":\\\"Fedor Indutny <fedor@indutny.com>\\\",\\\"license\\\":\\\"MIT\\\",\\\"bugs\\\":{\\\"url\\\":\\\"https://github.com/indutny/elliptic/issues\\\"},\\\"homepage\\\":\\\"https://github.com/indutny/elliptic\\\",\\\"devDependencies\\\":{\\\"brfs\\\":\\\"^2.0.2\\\",\\\"coveralls\\\":\\\"^3.1.0\\\",\\\"eslint\\\":\\\"^7.6.0\\\",\\\"grunt\\\":\\\"^1.2.1\\\",\\\"grunt-browserify\\\":\\\"^5.3.0\\\",\\\"grunt-cli\\\":\\\"^1.3.2\\\",\\\"grunt-contrib-connect\\\":\\\"^3.0.0\\\",\\\"grunt-contrib-copy\\\":\\\"^1.0.0\\\",\\\"grunt-contrib-uglify\\\":\\\"^5.0.0\\\",\\\"grunt-mocha-istanbul\\\":\\\"^5.0.2\\\",\\\"grunt-saucelabs\\\":\\\"^9.0.1\\\",\\\"istanbul\\\":\\\"^0.4.5\\\",\\\"mocha\\\":\\\"^8.0.1\\\"},\\\"dependencies\\\":{\\\"bn.js\\\":\\\"^4.11.9\\\",\\\"brorand\\\":\\\"^1.1.0\\\",\\\"hash.js\\\":\\\"^1.0.0\\\",\\\"hmac-drbg\\\":\\\"^1.0.1\\\",\\\"inherits\\\":\\\"^2.0.4\\\",\\\"minimalistic-assert\\\":\\\"^1.0.1\\\",\\\"minimalistic-crypto-utils\\\":\\\"^1.0.1\\\"},\\\"_resolved\\\":\\\"https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz\\\",\\\"_integrity\\\":\\\"sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==\\\",\\\"_from\\\":\\\"elliptic@6.5.4\\\"}\");\n\n//# sourceURL=webpack:///./node_modules/elliptic/package.json?");
2036
+ eval("module.exports = JSON.parse(\"{\\\"_args\\\":[[\\\"elliptic@6.5.4\\\",\\\"/home/erose/git/release/dcp\\\"]],\\\"_from\\\":\\\"elliptic@6.5.4\\\",\\\"_id\\\":\\\"elliptic@6.5.4\\\",\\\"_inBundle\\\":false,\\\"_integrity\\\":\\\"sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==\\\",\\\"_location\\\":\\\"/elliptic\\\",\\\"_phantomChildren\\\":{},\\\"_requested\\\":{\\\"type\\\":\\\"version\\\",\\\"registry\\\":true,\\\"raw\\\":\\\"elliptic@6.5.4\\\",\\\"name\\\":\\\"elliptic\\\",\\\"escapedName\\\":\\\"elliptic\\\",\\\"rawSpec\\\":\\\"6.5.4\\\",\\\"saveSpec\\\":null,\\\"fetchSpec\\\":\\\"6.5.4\\\"},\\\"_requiredBy\\\":[\\\"/browserify-sign\\\",\\\"/create-ecdh\\\",\\\"/ethereumjs-util\\\",\\\"/ethereumjs-wallet/ethereumjs-util\\\",\\\"/secp256k1\\\"],\\\"_resolved\\\":\\\"https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz\\\",\\\"_spec\\\":\\\"6.5.4\\\",\\\"_where\\\":\\\"/home/erose/git/release/dcp\\\",\\\"author\\\":{\\\"name\\\":\\\"Fedor Indutny\\\",\\\"email\\\":\\\"fedor@indutny.com\\\"},\\\"bugs\\\":{\\\"url\\\":\\\"https://github.com/indutny/elliptic/issues\\\"},\\\"dependencies\\\":{\\\"bn.js\\\":\\\"^4.11.9\\\",\\\"brorand\\\":\\\"^1.1.0\\\",\\\"hash.js\\\":\\\"^1.0.0\\\",\\\"hmac-drbg\\\":\\\"^1.0.1\\\",\\\"inherits\\\":\\\"^2.0.4\\\",\\\"minimalistic-assert\\\":\\\"^1.0.1\\\",\\\"minimalistic-crypto-utils\\\":\\\"^1.0.1\\\"},\\\"description\\\":\\\"EC cryptography\\\",\\\"devDependencies\\\":{\\\"brfs\\\":\\\"^2.0.2\\\",\\\"coveralls\\\":\\\"^3.1.0\\\",\\\"eslint\\\":\\\"^7.6.0\\\",\\\"grunt\\\":\\\"^1.2.1\\\",\\\"grunt-browserify\\\":\\\"^5.3.0\\\",\\\"grunt-cli\\\":\\\"^1.3.2\\\",\\\"grunt-contrib-connect\\\":\\\"^3.0.0\\\",\\\"grunt-contrib-copy\\\":\\\"^1.0.0\\\",\\\"grunt-contrib-uglify\\\":\\\"^5.0.0\\\",\\\"grunt-mocha-istanbul\\\":\\\"^5.0.2\\\",\\\"grunt-saucelabs\\\":\\\"^9.0.1\\\",\\\"istanbul\\\":\\\"^0.4.5\\\",\\\"mocha\\\":\\\"^8.0.1\\\"},\\\"files\\\":[\\\"lib\\\"],\\\"homepage\\\":\\\"https://github.com/indutny/elliptic\\\",\\\"keywords\\\":[\\\"EC\\\",\\\"Elliptic\\\",\\\"curve\\\",\\\"Cryptography\\\"],\\\"license\\\":\\\"MIT\\\",\\\"main\\\":\\\"lib/elliptic.js\\\",\\\"name\\\":\\\"elliptic\\\",\\\"repository\\\":{\\\"type\\\":\\\"git\\\",\\\"url\\\":\\\"git+ssh://git@github.com/indutny/elliptic.git\\\"},\\\"scripts\\\":{\\\"lint\\\":\\\"eslint lib test\\\",\\\"lint:fix\\\":\\\"npm run lint -- --fix\\\",\\\"test\\\":\\\"npm run lint && npm run unit\\\",\\\"unit\\\":\\\"istanbul test _mocha --reporter=spec test/index.js\\\",\\\"version\\\":\\\"grunt dist && git add dist/\\\"},\\\"version\\\":\\\"6.5.4\\\"}\");\n\n//# sourceURL=webpack:///./node_modules/elliptic/package.json?");
2037
2037
 
2038
2038
  /***/ }),
2039
2039
 
@@ -4680,17 +4680,6 @@ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _val
4680
4680
 
4681
4681
  /***/ }),
4682
4682
 
4683
- /***/ "./node_modules/valid-data-url/index.js":
4684
- /*!**********************************************!*\
4685
- !*** ./node_modules/valid-data-url/index.js ***!
4686
- \**********************************************/
4687
- /*! no static exports found */
4688
- /***/ (function(module, exports, __webpack_require__) {
4689
-
4690
- eval("var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;/*global define */\n\n// Detecting data URLs\n// data URI - MDN https://developer.mozilla.org/en-US/docs/data_URIs\n// The \"data\" URL scheme: http://tools.ietf.org/html/rfc2397\n// Valid URL Characters: http://tools.ietf.org/html/rfc2396#section2\n\n(function (root, factory) {\n // https://github.com/gotwarlost/istanbul/blob/master/ignoring-code-for-coverage.md#ignore-a-umd-wrapper\n /* istanbul ignore next */\n if (true) {\n !(__WEBPACK_AMD_DEFINE_ARRAY__ = [], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory),\n\t\t\t\t__WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ?\n\t\t\t\t(__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__),\n\t\t\t\t__WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));\n } else {}\n}(this, function () {\n 'use strict';\n\n function validDataUrl(s) {\n return validDataUrl.regex.test((s || '').trim());\n }\n validDataUrl.regex = /^data:([a-z]+\\/[a-z0-9-+.]+(;[a-z0-9-.!#$%*+.{}|~`]+=[a-z0-9-.!#$%*+.{}()|~`]+)*)?(;base64)?,([a-z0-9!$&',()*+;=\\-._~:@\\/?%\\s]*?)$/i;\n\n return validDataUrl;\n}));\n\n\n//# sourceURL=webpack:///./node_modules/valid-data-url/index.js?");
4691
-
4692
- /***/ }),
4693
-
4694
4683
  /***/ "./node_modules/wcwidth/combining.js":
4695
4684
  /*!*******************************************!*\
4696
4685
  !*** ./node_modules/wcwidth/combining.js ***!
@@ -4855,7 +4844,7 @@ eval("/**\n * @file src/dcp-error.js\n * @author Ryan Rossiter, ryan
4855
4844
  /*! no static exports found */
4856
4845
  /***/ (function(module, exports, __webpack_require__) {
4857
4846
 
4858
- eval("/* WEBPACK VAR INJECTION */(function(setImmediate) {/**\n * @author Wes Garland, wes@page.ca\n * @date Dec 2012, Jun 2018\n */\n\n\nconst LISTENERS = Symbol('EventEmitter Listeners');\nconst caseSensitiveEventNames = false; /* default false - DOM-level-2 compliant */\n\n/** An EventEmitter class which is API-compatible with Node's EventEmitter class. \n * This class essentially registers and executes callbacks by attaching them to strings.\n * There is no standard `event` object at the moment, so callbacks simply are passed whatever args the \n * registered event passes.\n * @memberof module:dcp/dcp-events\n * @access public\n */\nclass EventEmitter {\n constructor(debugLabel) {\n this.debugLabel = debugLabel || \"\";\n if (true) {\n this.debug = (__webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope('event-emitter'))(this.debugLabel || 'all');\n }\n }\n\n /**\n * Allow others to see the list of listeners. The underscores here are used to indicate\n * that you probably shouldn't need this. It is private to avoid polluting classes and is\n * for internal use only. Meaning this class or a subclass which actually need to mess\n * with internals like vemit or addListener.\n */\n get __listeners() { return this[LISTENERS] }\n\n /** Add an event listener. \n * @param {string} eventName The name of the event to capture, or true to capture all events\n * @param {function} eventHandler The function to invoke when the eventHandler is invoked. The arguments\n * to the function are specified by the code emitting the event, however\n * a final argument, eventName, is appended to event handlers invoked via\n * catch-alls. \n * @access public\n */\n addListener(eventName, eventHandler) {\n\n if (!this.hasOwnProperty(LISTENERS)) \n this[LISTENERS] = {}; /* must not be moved to ctor! */\n \n eventName = this.sanitizeEventName(eventName);\n if (!this[LISTENERS].hasOwnProperty(eventName))\n this[LISTENERS][eventName] = [];\n\n if (typeof eventHandler !== \"function\")\n throw new Error(\"eventHandler is not a function\");\n\n this[LISTENERS][eventName].push(eventHandler);\n this.emit(\"newListener\", eventName, eventHandler);\n }\n\n /**\n * Alias of {@link module:dcp/dcp-events.EventEmitter#addListener|addListener}.\n * @access public\n */\n on(eventName, eventHandler) {\n this.addListener(eventName, eventHandler);\n }\n\n /**\n * Alias of {@link module:dcp/dcp-events.EventEmitter#addListener|addListener}.\n * @access public\n */\n addEventListener(eventName, eventHandler) {\n this.addListener(eventName, eventHandler);\n }\n\n /**\n * Attach a callback (eventHandler) to a named event. It will be called once when this event emitter\n * emits that event and then removed.\n * @param {string} eventName Name of event\n * @param {function} eventHandler Function to call\n * @access public\n */\n once(eventName, eventHandler) {\n function onceWrapper() {\n eventHandler.apply(eventHandler, arguments);\n this.removeListener(eventName, onceWrapper);\n }\n this.addListener(eventName, onceWrapper);\n return onceWrapper;\n }\n\n /**\n * Remove a listener (`eventHandler`) attached to a named event. It must\n * be the same `eventHandler` instance that was originally attached.\n * @param {string} eventName Name of event\n * @param {function} eventHandler Function to call\n * @access public\n */\n removeListener(eventName, eventHandler) {\n var i;\n\n eventName = this.sanitizeEventName(eventName);\n if (!this[LISTENERS] || !this[LISTENERS].hasOwnProperty(eventName))\n return;\n\n i = this[LISTENERS][eventName].indexOf(eventHandler);\n if (i != -1)\n {\n this[LISTENERS][eventName].splice(i, 1);\n if (this[LISTENERS][eventName].length === 0)\n delete this[LISTENERS][eventName];\n }\n }\n\n /**\n * Alias of {@link module:dcp/dcp-events.EventEmitter#removeListener|removeListener}.\n * @access public\n */\n off(eventName, eventHandler) {\n this.removeListener(eventName, eventHandler)\n }\n\n /**\n * Alias of {@link module:dcp/dcp-events.EventEmitter#removeListener|removeListener}.\n * @access public\n */\n removeEventListener(eventName, eventHandler) {\n this.removeListener(eventName, eventHandler)\n }\n\n removeAllListeners(eventName) {\n if (typeof eventName !== 'undefined') {\n eventName = this.sanitizeEventName(eventName);\n if (this[LISTENERS])\n delete this[LISTENERS][eventName];\n } else {\n this[LISTENERS] = {};\n }\n }\n\n listeners(eventName) {\n if (!this[LISTENERS])\n return [];\n \n eventName = this.sanitizeEventName(eventName);\n if (!this[LISTENERS].hasOwnProperty(eventName))\n this[LISTENERS][eventName] = [];\n\n return this[LISTENERS][eventName];\n }\n\n setMaxListeners(number) {\n throw new Error(\"not implemented\");\n }\n\n listenerCount(eventName) {\n if (!this[LISTENERS])\n return 0;\n \n eventName = this.sanitizeEventName(eventName);\n return this[LISTENERS][eventName]? this[LISTENERS][eventName].length : 0;\n }\n\n sanitizeEventName(eventName) {\n if (!caseSensitiveEventNames)\n eventName = eventName.toLowerCase();\n return eventName;\n }\n\n /** Event emitter. All interfaces which emit events eventually fall through to this function. \n * This function invokes event listeners in the correct context for the passed eventName, as\n * well as any catch-all (true) listeners.\n *\n * @param _eventName The name of the event to emit.\n * @param args An array (not array-like) object containing the arguments \n * to pass to the listener.\n */\n vemit(_eventName, args) {\n var i;\n const eventName = this.sanitizeEventName(_eventName);\n\n if (!this[LISTENERS] || !(this[LISTENERS].hasOwnProperty(eventName) || this[LISTENERS].hasOwnProperty(true))) {\n if (this.debug)\n console.log(\"Not firing \" + this.debugLabel + \"::\" + _eventName + \" -- no listeners\");\n return;\n }\n\n if (this.debug)\n console.log(\"Firing \" + this.debugLabel + \"::\" + _eventName);\n if (this[LISTENERS].hasOwnProperty(eventName))\n for (i=0; i < this[LISTENERS][eventName].length; i++)\n this.runSoon(this, this[LISTENERS][eventName][i], args);\n if (typeof this[LISTENERS][true] !== \"undefined\")\n for (i=0; i < this[LISTENERS][true].length; i++)\n this.runSoon(this, this[LISTENERS][true][i], args.concat(eventName));\n }\n\n /** Emit an event. \n *\n * @param {string} eventName The event to emit\n * @param ... Any other arguments passed will be passed exactly to the listener.\n * @access public\n */\n emit(eventName /* ... */) {\n var args;\n\n args = Array.prototype.slice.call(arguments);\n args.shift();\n\n return this.vemit(eventName, args);\n }\n\n /** Proxy all of the events from another EventEmitter or DOM EventTarget and \n * re-emit them as though they originated from this EventEmitter.\n * \n * @param element The EventEmitter / EventTarget whose events we are to re-emit\n * @param eventName ... One or more arguments which are strings that contain the names of\n * the events to re-emit.\n */\n proxy(element, eventName /*, eventName... */) {\n var i, fun;\n var _this = this;\n\n for (i=1; i < arguments.length; i++) {\n fun = (function events$$EventEmitter$proxy$fun(thisEventName) {\n this.sanitizeEventName(thisEventName);\n return (function events$$EventEmitter$proxy$fun$bound() {\n if (typeof arguments[0] === 'object')\n arguments[0].proxyType = thisEventName;\n this.vemit(thisEventName, arguments) \n }).bind(_this);\n })(arguments[i]);\n element.addEventListener(arguments[i], fun);\n }\n }\n\n /** Proxy an events from another EventEmitter or DOM EventTarget and \n * re-emit them as though they originated from this EventEmitter, under a new event type.\n * \n * @param element The EventEmitter / EventTarget whose events we are to re-emit\n * @param captureEventName The name of the event to re-emit, or true to re-emit all events\n * @param newEventName The event type of the new event when eventName is not true; when\n * eventName is true, it is used as a prefix prepended to the real\n * event name (ev.type)\n *\n * *note* - Catch-all proxies are only supported on instances of events$$EventEmitter. That\n * functionality is not supported by DOM events.\n */\n proxyAs(element, captureEventName, emitterEventName) {\n var fun;\n captureEventName = this.sanitizeEventName(captureEventName);\n emitterEventName = this.sanitizeEventName(emitterEventName);\n \n if (element === this && captureEventName === true)\n throw new Error(\"Infinite recursion is not supported\");\n\n fun = (function events$$EventEmitter$proxyAs$eventHandler(ev) {\n var args = Array.prototype.slice.call(arguments);\n\n if (typeof ev === 'object' && ev.hasOwnProperty('type'))\n ev.proxyType = ev.type;\n\n if (captureEventName === true)\n this.vemit(emitterEventName + args.pop(), args);\n else\n this.vemit(emitterEventName, arguments);\n }).bind(this);\n\n element.addEventListener(captureEventName, fun);\n }\n\n /** Run this supplied function ASAP in the reactor loop, with the provided 'this' and arguments.\n * @param _this [optional] An object to use apply as 'this' when invoking fn\n * @param fn The function to invoke\n * @param args Array of arguments passed to fn during invocation\n */\n runSoon(_this, fn, args) {\n var callback;\n\n if (typeof _this === \"function\" && typeof args === 'undefined') {\n args = fn;\n fn = _this;\n _this = null;\n }\n\n if (typeof fn !== \"function\")\n throw new Error(\"fn is not a function\");\n \n if (typeof args === 'object')\n callback = function runSoon_inner() { return fn.apply(_this, args) };\n else\n callback = function runSoon_inner() { return fn.apply(_this, []) };\n \n return this.doCallback(callback);\n }\n\n get doCallback() { \n return typeof setImmediate === 'function' ? setImmediate : (cb) => { setTimeout(cb, 0) };\n }\n\n /** Debug interface - not stable */\n dumpKnownEvents() {\n var eventName, i;\n var list = [];\n\n if (!this[LISTENERS] || !this[LISTENERS].length)\n return list;\n\n for (eventName in this[LISTENERS])\n list.push(eventName);\n \n list.sort();\n for (i=0; i < list.length; i++) {\n if (list[i] == list[i+1])\n list.splice(i+1,1);\n }\n\n return list;\n }\n}\n\n\nexports.EventEmitter = EventEmitter;\nexports.caseSensitiveEventNames = caseSensitiveEventNames;\n\n/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(/*! ./../../../node_modules/timers-browserify/main.js */ \"./node_modules/timers-browserify/main.js\").setImmediate))\n\n//# sourceURL=webpack:///./src/common/dcp-events/event-emitter.js?");
4847
+ eval("/* WEBPACK VAR INJECTION */(function(setImmediate) {/**\n * @author Wes Garland, wes@page.ca\n * @date Dec 2012, Jun 2018\n */\n\n\nconst LISTENERS = Symbol('EventEmitter Listeners');\nconst caseSensitiveEventNames = false; /* default false - DOM-level-2 compliant */\n\n/** An EventEmitter class which is API-compatible with Node's EventEmitter class. \n * This class essentially registers and executes callbacks by attaching them to strings.\n * There is no standard `event` object at the moment, so callbacks simply are passed whatever args the \n * registered event passes.\n * @memberof module:dcp/dcp-events\n * @access public\n */\nclass EventEmitter {\n constructor(debugLabel) {\n this.debugLabel = debugLabel || \"\";\n if (true) {\n this.debug = (__webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope('event-emitter'))(this.debugLabel || 'all');\n }\n }\n\n /**\n * Allow others to see the list of listeners. The underscores here are used to indicate\n * that you probably shouldn't need this. It is private to avoid polluting classes and is\n * for internal use only. Meaning this class or a subclass which actually need to mess\n * with internals like vemit or addListener.\n */\n get __listeners() { return this[LISTENERS] }\n\n /** Add an event listener. \n * @param {string} eventName The name of the event to capture, or true to capture all events\n * @param {function} eventHandler The function to invoke when the eventHandler is invoked. The arguments\n * to the function are specified by the code emitting the event, however\n * a final argument, eventName, is appended to event handlers invoked via\n * catch-alls. \n * @access public\n */\n addListener(eventName, eventHandler) {\n\n if (!this.hasOwnProperty(LISTENERS)) \n this[LISTENERS] = {}; /* must not be moved to ctor! */\n \n eventName = this.sanitizeEventName(eventName);\n if (!this[LISTENERS].hasOwnProperty(eventName))\n this[LISTENERS][eventName] = [];\n\n if (typeof eventHandler !== \"function\")\n throw new Error(\"eventHandler is not a function\");\n\n this[LISTENERS][eventName].push(eventHandler);\n this.emit(\"newListener\", eventName, eventHandler);\n }\n\n /**\n * Alias of {@link module:dcp/dcp-events.EventEmitter#addListener|addListener}.\n * @access public\n */\n on(eventName, eventHandler) {\n this.addListener(eventName, eventHandler);\n }\n\n /**\n * Alias of {@link module:dcp/dcp-events.EventEmitter#addListener|addListener}.\n * @access public\n */\n addEventListener(eventName, eventHandler) {\n this.addListener(eventName, eventHandler);\n }\n\n /**\n * Attach a callback (eventHandler) to a named event. It will be called once when this event emitter\n * emits that event and then removed.\n * @param {string} eventName Name of event\n * @param {function} eventHandler Function to call\n * @access public\n */\n once(eventName, eventHandler) {\n function onceWrapper() {\n eventHandler.apply(eventHandler, arguments);\n this.removeListener(eventName, onceWrapper);\n }\n this.addListener(eventName, onceWrapper);\n return onceWrapper;\n }\n\n /**\n * Remove a listener (`eventHandler`) attached to a named event. It must\n * be the same `eventHandler` instance that was originally attached.\n * @param {string} eventName Name of event\n * @param {function} eventHandler Function to call\n * @access public\n */\n removeListener(eventName, eventHandler) {\n var i;\n\n eventName = this.sanitizeEventName(eventName);\n if (!this[LISTENERS] || !this[LISTENERS].hasOwnProperty(eventName))\n return;\n\n i = this[LISTENERS][eventName].indexOf(eventHandler);\n if (i != -1)\n {\n this[LISTENERS][eventName].splice(i, 1);\n if (this[LISTENERS][eventName].length === 0)\n delete this[LISTENERS][eventName];\n }\n }\n\n /**\n * Alias of {@link module:dcp/dcp-events.EventEmitter#removeListener|removeListener}.\n * @access public\n */\n off(eventName, eventHandler) {\n this.removeListener(eventName, eventHandler)\n }\n\n /**\n * Alias of {@link module:dcp/dcp-events.EventEmitter#removeListener|removeListener}.\n * @access public\n */\n removeEventListener(eventName, eventHandler) {\n this.removeListener(eventName, eventHandler)\n }\n\n removeAllListeners(eventName) {\n if (typeof eventName !== 'undefined') {\n eventName = this.sanitizeEventName(eventName);\n if (this[LISTENERS])\n delete this[LISTENERS][eventName];\n } else {\n this[LISTENERS] = {};\n }\n }\n\n listeners(eventName) {\n if (!this[LISTENERS])\n return [];\n \n eventName = this.sanitizeEventName(eventName);\n if (!this[LISTENERS].hasOwnProperty(eventName))\n this[LISTENERS][eventName] = [];\n\n return this[LISTENERS][eventName];\n }\n\n setMaxListeners(number) {\n throw new Error(\"not implemented\");\n }\n\n listenerCount(eventName) {\n if (!this[LISTENERS])\n return 0;\n \n eventName = this.sanitizeEventName(eventName);\n return this[LISTENERS][eventName]? this[LISTENERS][eventName].length : 0;\n }\n\n sanitizeEventName(eventName) {\n if (!caseSensitiveEventNames)\n eventName = String(eventName).toLowerCase();\n return eventName;\n }\n\n /** Event emitter. All interfaces which emit events eventually fall through to this function. \n * This function invokes event listeners in the correct context for the passed eventName, as\n * well as any catch-all (true) listeners.\n *\n * @param _eventName The name of the event to emit.\n * @param args An array (not array-like) object containing the arguments \n * to pass to the listener.\n */\n vemit(_eventName, args) {\n var i;\n const eventName = this.sanitizeEventName(_eventName);\n\n if (!this[LISTENERS] || !(this[LISTENERS].hasOwnProperty(eventName) || this[LISTENERS].hasOwnProperty(true))) {\n if (this.debug)\n console.log(\"Not firing \" + this.debugLabel + \"::\" + _eventName + \" -- no listeners\");\n return;\n }\n\n if (this.debug)\n console.log(\"Firing \" + this.debugLabel + \"::\" + _eventName);\n if (this[LISTENERS].hasOwnProperty(eventName))\n for (i=0; i < this[LISTENERS][eventName].length; i++)\n this.runSoon(this, this[LISTENERS][eventName][i], args);\n if (typeof this[LISTENERS][true] !== \"undefined\")\n for (i=0; i < this[LISTENERS][true].length; i++)\n this.runSoon(this, this[LISTENERS][true][i], args.concat(eventName));\n }\n\n /** Emit an event. \n *\n * @param {string} eventName The event to emit\n * @param ... Any other arguments passed will be passed exactly to the listener.\n * @access public\n */\n emit(eventName /* ... */) {\n var args;\n\n args = Array.prototype.slice.call(arguments);\n args.shift();\n\n return this.vemit(eventName, args);\n }\n\n /** Proxy all of the events from another EventEmitter or DOM EventTarget and \n * re-emit them as though they originated from this EventEmitter.\n * \n * @param element The EventEmitter / EventTarget whose events we are to re-emit\n * @param eventName ... One or more arguments which are strings that contain the names of\n * the events to re-emit.\n */\n proxy(element, eventName /*, eventName... */) {\n var i, fun;\n var _this = this;\n\n for (i=1; i < arguments.length; i++) {\n fun = (function events$$EventEmitter$proxy$fun(thisEventName) {\n this.sanitizeEventName(thisEventName);\n return (function events$$EventEmitter$proxy$fun$bound() {\n if (typeof arguments[0] === 'object')\n arguments[0].proxyType = thisEventName;\n this.vemit(thisEventName, arguments) \n }).bind(_this);\n })(arguments[i]);\n element.addEventListener(arguments[i], fun);\n }\n }\n\n /** Proxy an events from another EventEmitter or DOM EventTarget and \n * re-emit them as though they originated from this EventEmitter, under a new event type.\n * \n * @param element The EventEmitter / EventTarget whose events we are to re-emit\n * @param captureEventName The name of the event to re-emit, or true to re-emit all events\n * @param newEventName The event type of the new event when eventName is not true; when\n * eventName is true, it is used as a prefix prepended to the real\n * event name (ev.type)\n *\n * *note* - Catch-all proxies are only supported on instances of events$$EventEmitter. That\n * functionality is not supported by DOM events.\n */\n proxyAs(element, captureEventName, emitterEventName) {\n var fun;\n captureEventName = this.sanitizeEventName(captureEventName);\n emitterEventName = this.sanitizeEventName(emitterEventName);\n \n if (element === this && captureEventName === true)\n throw new Error(\"Infinite recursion is not supported\");\n\n fun = (function events$$EventEmitter$proxyAs$eventHandler(ev) {\n var args = Array.prototype.slice.call(arguments);\n\n if (typeof ev === 'object' && ev.hasOwnProperty('type'))\n ev.proxyType = ev.type;\n\n if (captureEventName === true)\n this.vemit(emitterEventName + args.pop(), args);\n else\n this.vemit(emitterEventName, arguments);\n }).bind(this);\n\n element.addEventListener(captureEventName, fun);\n }\n\n /** Run this supplied function ASAP in the reactor loop, with the provided 'this' and arguments.\n * @param _this [optional] An object to use apply as 'this' when invoking fn\n * @param fn The function to invoke\n * @param args Array of arguments passed to fn during invocation\n */\n runSoon(_this, fn, args) {\n var callback;\n\n if (typeof _this === \"function\" && typeof args === 'undefined') {\n args = fn;\n fn = _this;\n _this = null;\n }\n\n if (typeof fn !== \"function\")\n throw new Error(\"fn is not a function\");\n \n if (typeof args === 'object')\n callback = function runSoon_inner() { return fn.apply(_this, args) };\n else\n callback = function runSoon_inner() { return fn.apply(_this, []) };\n \n return this.doCallback(callback);\n }\n\n get doCallback() { \n return typeof setImmediate === 'function' ? setImmediate : (cb) => { setTimeout(cb, 0) };\n }\n\n /** Debug interface - not stable */\n dumpKnownEvents() {\n var eventName, i;\n var list = [];\n\n if (!this[LISTENERS] || !this[LISTENERS].length)\n return list;\n\n for (eventName in this[LISTENERS])\n list.push(eventName);\n \n list.sort();\n for (i=0; i < list.length; i++) {\n if (list[i] == list[i+1])\n list.splice(i+1,1);\n }\n\n return list;\n }\n}\n\n\nexports.EventEmitter = EventEmitter;\nexports.caseSensitiveEventNames = caseSensitiveEventNames;\n\n/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(/*! ./../../../node_modules/timers-browserify/main.js */ \"./node_modules/timers-browserify/main.js\").setImmediate))\n\n//# sourceURL=webpack:///./src/common/dcp-events/event-emitter.js?");
4859
4848
 
4860
4849
  /***/ }),
4861
4850
 
@@ -4877,7 +4866,7 @@ eval("/**\n * @file event-interceptor.js\n * Implement a cla
4877
4866
  /*! no static exports found */
4878
4867
  /***/ (function(module, exports, __webpack_require__) {
4879
4868
 
4880
- eval("exports.EventInterceptor = __webpack_require__(/*! ./event-interceptor */ \"./src/common/dcp-events/event-interceptor.js\").EventInterceptor;\nexports.EventEmitter = __webpack_require__(/*! ./event-emitter */ \"./src/common/dcp-events/event-emitter.js\").EventEmitter;\nexports.PropagatingEventEmitter = __webpack_require__(/*! ./propagating-event-emitter */ \"./src/common/dcp-events/propagating-event-emitter.js\").PropagatingEventEmitter;\n\n\n//# sourceURL=webpack:///./src/common/dcp-events/index.js?");
4869
+ eval("exports.EventInterceptor = __webpack_require__(/*! ./event-interceptor */ \"./src/common/dcp-events/event-interceptor.js\").EventInterceptor;\nexports.EventEmitter = __webpack_require__(/*! ./event-emitter */ \"./src/common/dcp-events/event-emitter.js\").EventEmitter;\nexports.PropagatingEventEmitter = __webpack_require__(/*! ./propagating-event-emitter */ \"./src/common/dcp-events/propagating-event-emitter.js\").PropagatingEventEmitter;\n\n\n\n// Event constants\nexports.eventTypes = { };\n[\n 'nofunds',\n 'cancel',\n 'readyStateChange', // @TODO - change this to be emitted from the scheduler as the scheduler says it reaches specific points in setting up the job.\n 'complete', // @TODO - ensure this event works. It might not be needed / might need a name change.\n].forEach(eventName => exports.eventTypes[eventName] = { reliable: true });\n\n[\n 'console',\n 'result',\n 'error',\n 'status',\n 'stop',\n 'noprogress',\n 'custom',\n 'schedmsg::command',\n 'schedmsg::broadcast',\n].forEach(eventName => exports.eventTypes[eventName] = { reliable: false });\n\nexports.events = {};\n\nObject.keys(exports.eventTypes).forEach(eventName => exports.events[eventName] = eventName);\n\n\n//# sourceURL=webpack:///./src/common/dcp-events/index.js?");
4881
4870
 
4882
4871
  /***/ }),
4883
4872
 
@@ -5065,7 +5054,7 @@ eval("/**\n * @file password.js\n * Modal providing a way to
5065
5054
  /*! no static exports found */
5066
5055
  /***/ (function(module, exports, __webpack_require__) {
5067
5056
 
5068
- 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=dd98e423ca01eaeeba22e0bd0d948e358ffc7b43'; /* installer token */\n return '?ucp=' + Date.now();\n}\n \n/* Detect load type - on webpack, load dynamic content relative to webpack bundle;\n * otherwise load relative to the current scheduler's configured portal.\n */\nexports.myScript = (typeof document !== 'undefined') && document.currentScript;\nexports.corsProxyHref = undefined;\nif (exports.myScript && exports.myScript === __webpack_require__(/*! ./fetch-relative */ \"./src/dcp-client/client-modal/fetch-relative.js\").myScript) {\n let url = new (__webpack_require__(/*! dcp/common/dcp-url */ \"./src/common/dcp-url.js\").DcpURL)(exports.myScript.src);\n exports.corsProxyHref = url.resolve('../cors-proxy.html');\n}\n\n/**\n * Look for modal id and required ids on page based on config, if not found, provide from dcp-client.\n * The first id in the required array must be the id of the modal's form element.\n * @param {Object} modalConfig Modal configuration object\n * @param {string} modalConfig.id Id of parent modal element\n * @param {string[]} modalConfig.required Array of required ids in parent modal element\n * @param {string[]} [modalConfig.optional] Array of optional ids in parent modal element\n * @param {string} modalConfig.path Relative path to modal html in dcp-client\n * @returns {DOMElement[]} Array of modal elements on page [config.id, ...config.required]\n */\nexports.initModal = async function (modalConfig, onClose) {\n exports.corsProxyHref = exports.corsProxyHref || dcpConfig.portal.location.resolve('dcp-client/cors-proxy.html');\n\n // Call ensure modal on any eager-loaded modals.\n if (modalConfig.eagerLoad) {\n Promise.all(\n modalConfig.eagerLoad.map(config => ensureModal(config))\n )\n };\n\n const [elements, optionalElements] = await ensureModal(modalConfig);\n\n // Wire up form to prevent default, resolve on submission, reject+reset when closed (or call onClose when closed)\n const [modal, form] = elements;\n form.reset(); // ensure that form is fresh\n let formResolve, formReject;\n let formPromise = new Promise( function(res, rej) {\n formResolve = res;\n formReject = rej;\n });\n form.onsubmit = function (submitEvent) {\n submitEvent.preventDefault();\n modal.setAttribute(\"data-state\", \"submitted\");\n formResolve(submitEvent);\n }\n\n exports.MicroModal.show(modalConfig.id, { \n disableFocus: true, \n onClose: onClose || getDefaultOnClose(formReject)\n });\n return [elements, formPromise, optionalElements];\n};\n\n// Ensure all required modal elements are on page according to modalConfig\nasync function ensureModal(modalConfig) {\n let allRequiredIds = [modalConfig.id, ...modalConfig.required];\n let missing = allRequiredIds.filter( id => !document.getElementById(id) );\n if (missing.length > 0) {\n if (missing.length !== allRequiredIds.length)\n console.warn(`Some of the ids needed to replace the default DCP-modal were found, but not all. So the default DCP-Modal will be used. Missing ids are: [${missing}].`);\n let contents = await fetchRelative(exports.corsProxyHref, modalConfig.path + cachePoison());\n const container = document.createElement('div');\n container.innerHTML = contents;\n document.body.appendChild(container);\n }\n\n const elements = allRequiredIds.map(id => document.getElementById(id));\n const optionalElements = (modalConfig.optional || []).map(id => document.getElementById(id));\n return [elements, optionalElements];\n};\n\n// This onClose is called by MicroModal and thus has the modal passed to it.\nfunction getDefaultOnClose (formReject) {\n return (modal) => {\n modal.offsetLeft; // forces style recalc\n const origState = modal.dataset.state;\n // reset form including data-state\n modal.setAttribute(\"data-state\", \"new\");\n // reject if closed without submitting form.\n if (origState !== \"submitted\") {\n const err = new DCPError(\"Modal was closed but modal's form was not submitted.\", exports.OnCloseErrorCode);\n formReject(err);\n }\n }\n}\n\n\n//# sourceURL=webpack:///./src/dcp-client/client-modal/utils.js?");
5057
+ 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=e4988ba0b1c993e062fc7c536188bda3ea4de883'; /* installer token */\n return '?ucp=' + Date.now();\n}\n \n/* Detect load type - on webpack, load dynamic content relative to webpack bundle;\n * otherwise load relative to the current scheduler's configured portal.\n */\nexports.myScript = (typeof document !== 'undefined') && document.currentScript;\nexports.corsProxyHref = undefined;\nif (exports.myScript && exports.myScript === __webpack_require__(/*! ./fetch-relative */ \"./src/dcp-client/client-modal/fetch-relative.js\").myScript) {\n let url = new (__webpack_require__(/*! dcp/common/dcp-url */ \"./src/common/dcp-url.js\").DcpURL)(exports.myScript.src);\n exports.corsProxyHref = url.resolve('../cors-proxy.html');\n}\n\n/**\n * Look for modal id and required ids on page based on config, if not found, provide from dcp-client.\n * The first id in the required array must be the id of the modal's form element.\n * @param {Object} modalConfig Modal configuration object\n * @param {string} modalConfig.id Id of parent modal element\n * @param {string[]} modalConfig.required Array of required ids in parent modal element\n * @param {string[]} [modalConfig.optional] Array of optional ids in parent modal element\n * @param {string} modalConfig.path Relative path to modal html in dcp-client\n * @returns {DOMElement[]} Array of modal elements on page [config.id, ...config.required]\n */\nexports.initModal = async function (modalConfig, onClose) {\n exports.corsProxyHref = exports.corsProxyHref || dcpConfig.portal.location.resolve('dcp-client/cors-proxy.html');\n\n // Call ensure modal on any eager-loaded modals.\n if (modalConfig.eagerLoad) {\n Promise.all(\n modalConfig.eagerLoad.map(config => ensureModal(config))\n )\n };\n\n const [elements, optionalElements] = await ensureModal(modalConfig);\n\n // Wire up form to prevent default, resolve on submission, reject+reset when closed (or call onClose when closed)\n const [modal, form] = elements;\n form.reset(); // ensure that form is fresh\n let formResolve, formReject;\n let formPromise = new Promise( function(res, rej) {\n formResolve = res;\n formReject = rej;\n });\n form.onsubmit = function (submitEvent) {\n submitEvent.preventDefault();\n modal.setAttribute(\"data-state\", \"submitted\");\n formResolve(submitEvent);\n }\n\n exports.MicroModal.show(modalConfig.id, { \n disableFocus: true, \n onClose: onClose || getDefaultOnClose(formReject)\n });\n return [elements, formPromise, optionalElements];\n};\n\n// Ensure all required modal elements are on page according to modalConfig\nasync function ensureModal(modalConfig) {\n let allRequiredIds = [modalConfig.id, ...modalConfig.required];\n let missing = allRequiredIds.filter( id => !document.getElementById(id) );\n if (missing.length > 0) {\n if (missing.length !== allRequiredIds.length)\n console.warn(`Some of the ids needed to replace the default DCP-modal were found, but not all. So the default DCP-Modal will be used. Missing ids are: [${missing}].`);\n let contents = await fetchRelative(exports.corsProxyHref, modalConfig.path + cachePoison());\n const container = document.createElement('div');\n container.innerHTML = contents;\n document.body.appendChild(container);\n }\n\n const elements = allRequiredIds.map(id => document.getElementById(id));\n const optionalElements = (modalConfig.optional || []).map(id => document.getElementById(id));\n return [elements, optionalElements];\n};\n\n// This onClose is called by MicroModal and thus has the modal passed to it.\nfunction getDefaultOnClose (formReject) {\n return (modal) => {\n modal.offsetLeft; // forces style recalc\n const origState = modal.dataset.state;\n // reset form including data-state\n modal.setAttribute(\"data-state\", \"new\");\n // reject if closed without submitting form.\n if (origState !== \"submitted\") {\n const err = new DCPError(\"Modal was closed but modal's form was not submitted.\", exports.OnCloseErrorCode);\n formReject(err);\n }\n }\n}\n\n\n//# sourceURL=webpack:///./src/dcp-client/client-modal/utils.js?");
5069
5058
 
5070
5059
  /***/ }),
5071
5060
 
@@ -5087,7 +5076,7 @@ eval("/**\n * @file Client facing module that implements Compute Groups API\n
5087
5076
  /*! no static exports found */
5088
5077
  /***/ (function(module, exports, __webpack_require__) {
5089
5078
 
5090
- eval("/**\n * @file Module that implements Compute API\n * @module dcp/compute\n * @access public\n */\n\n/* global dcpConfig */\n\n/**\n * @access private - Will change soon!\n * @typedef {object} JobRequirements\n * @property {object} environment\n * @property {boolean} environment.fdlibm - Workers express capability 'fdlibm' in order for client applications to have confidence that results will be bitwise identical across workers. This library is recommended, but not required, for implementaiton of Google chrome, Mozilla Firefox and the DCP standalone worker use fdlibm but Microsoft Edge and Apple Safari do not use it.\n * @property {boolean} environment.offscreenCanvas When present, this capability indicates that the worker environment has the OffscreenCanvas symbol defined.\n * @property {object} details\n * @property {object} details.offscreenCanvas\n * @property {boolean} details.offscreenCanvas.bigTexture4096 This capability indicates that the worker's WebGL MAX_TEXTURE_SIZE is at least 4096.\n * @property {boolean} details.offscreenCanvas.bigTexture8192 This capability indicates that the worker's WebGL MAX_TEXTURE_SIZE is at least 8192.\n * @property {boolean} details.offscreenCanvas.bigTexture16384 This capability indicates that the worker's WebGL MAX_TEXTURE_SIZE is at least 16384.\n * @property {boolean} details.offscreenCanvas.bigTexture32768 This capability indicates that the worker's WebGL MAX_TEXTURE_SIZE is at least 32768.\n * @property {object} engine\n * @property {boolean} engine.es7 This capability enforces that the worker is running an es7-compliant JavaScript engine.\n * @property {boolean} engine.spidermonkey The capability enforces that the worker will run in the SpiderMonkey JS engine.\n * @property {boolean} browser.chrome The capability to exclude Chrome browsers.\n */\n\n/**\n * This JSDoc 'namespace' is a convenient way to document some globals that are available only in\n * the sandbox environment provided to work functions.\n * @access public\n * @namespace sandboxEnv\n */\n\n/**\n * This function emits a progress event. Progress events should be emitted approximately once per second; a task which fails to emit a \n * progress event for a certain period of time will be cancelled by the supervisor. The argument to this function is interpreted to six \n * significant digits, and must increase for every call. *All work functions must emit at least one progress event* - this requirement \n * will be enforced by the estimator. * The period of time mentioned above will be at least 30 wall-clock seconds and at least 30 \n * benchmark-adjusted seconds\n * @access public\n * @method progress\n * @param {string|number|undefined} n a number between 0 and 1 (inclusive) that represents a best-guess \n * at the completed portion of the task as a ratio of completed work to total work. If the argument is a string ending in the `%` symbol, \n * it will be interpreted as a number in the usual mathematical sense. If the argument is `undefined`, the slice progress is noted but \n * the amount of progress is considered to be indeterminate.\n * @memberof module:dcp/compute~sandboxEnv\n * @returns {boolean} true\n */\n\n/**\n * Emit a `console` event to the `JobHandle` in the client. If the sandbox is running in an environment with a native console object, the native method may also be invoked.\n * \n * @member console\n * @property {function} log fire console event with 'log' as `level` (see {@link Job#event:console|Job#event:console.level}).\n * @property {function} debug fire console event with 'debug' as `level` (see {@link Job#event:console|Job#event:console.level}).\n * @property {function} info fire console event with 'info' as `level` (see {@link Job#event:console|Job#event:console.level}).\n * @property {function} warn fire console event with 'warn' as `level` (see {@link Job#event:console|Job#event:console.level}).\n * @property {function} error fire console event with 'error' as `level` (see {@link Job#event:console|Job#event:console.level}).\n * \n * **Note** some ES environments (Chrome, Firefox) implement C-style print formatting in this method. This is currently not supported.\n * @memberof module:dcp/compute~sandboxEnv\n * @emits Job#console\n * @access public\n */\n\n/**\n * @member work An object that contains the following properties\n * @property {function} emit (eventName, ...) - send arbitrary args with `eventName`. This event is received client-side with those args (see {@link Job#work}).\n * @property {object} job\n * @property {object} job.public Property set on job handle, see {@link Job#public}.\n * @memberof module:dcp/compute~sandboxEnv\n * @access public\n */\n\n/**\n * @member {OffscreenCanvas} OffscreenCanvas As defined in the {@link https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvas|HTML Standard}, provides a canvas which can be rendered off-screen. If this interface is not available in a given worker, the worker will not report capability \"offscreenCanvas\".\n * @memberof module:dcp/compute~sandboxEnv\n * @access public\n */\n\nconst protocolV4 = __webpack_require__(/*! dcp/protocol-v4 */ \"./src/protocol-v4/index.js\");\nconst { EventEmitter } = __webpack_require__(/*! dcp/common/dcp-events/event-emitter */ \"./src/common/dcp-events/event-emitter.js\");\nconst { RangeObject, rehydrateRange } = __webpack_require__(/*! dcp/dcp-client/range-object */ \"./src/dcp-client/range-object.js\");\nconst RemoteDataSet = __webpack_require__(/*! dcp/dcp-client/remote-data-set */ \"./src/dcp-client/remote-data-set.js\");\nconst { Job, ResultHandle } = __webpack_require__(/*! dcp/dcp-client/job */ \"./src/dcp-client/job/index.js\");\nconst wallet = __webpack_require__(/*! dcp/dcp-client/wallet */ \"./src/dcp-client/wallet/index.js\");\nconst { DcpURL } = __webpack_require__(/*! dcp/common/dcp-url */ \"./src/common/dcp-url.js\");\nconst { disableWorker } = __webpack_require__(/*! dcp/dcp-client/worker */ \"./src/dcp-client/worker/index.js\");\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\nconst debugging = __webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope('dcp-client');\n\nconst debug = (...args) => {\n if (debugging()) {\n args.unshift('dcp-client:compute');\n console.debug(...args);\n }\n};\n\nconst MARKET_VALUE = Symbol('Market Value');\nconst indirectEval = eval; // eslint-disable-line no-eval\n\n/**\n * Instance methods of this class are exported as static members of {@link module:dcp/compute|dcp/compute}.\n * @example\n * let job = compute.for(1, 3, function (i) {\n * progress('100%');\n * return i*10;\n * })\n * let results = await job.exec();\n */\nclass Compute extends EventEmitter {\n constructor () {\n super('Compute');\n\n /** @todo move these into some central config */\n this.defaultMaxGPUs = 1;\n this.GPUsAssigned = 0;\n this.allowSlicesToFinishBeforeStopping = false;\n\n this.schedulerURL = undefined; // Will default to dcpConfig.scheduler.location if not overidden\n this.bankURL = undefined; // Will default to dcpConfig.bank.services.bankTeller.location if not overidden\n\n Object.defineProperty(this.marketValue, MARKET_VALUE, {\n value: true,\n writable: false,\n configurable: false,\n enumerable: false,\n });\n\n const ceci = this;\n\n // Initialize to null so these properties are recognized for the Job class\n this.phemeConnection = null;\n this.deployConnection = null;\n this.openPhemeConn = function openBankConn()\n {\n ceci.phemeConnection = new protocolV4.Connection(dcpConfig.scheduler.services.pheme);\n ceci.phemeConnection.on('close', ceci.openPhemeConn);\n }\n\n this.openDeployConn = function openDeployConn()\n {\n ceci.deployConnection = new protocolV4.Connection(dcpConfig.scheduler.services.jobSubmit);\n ceci.deployConnection.on('close', ceci.openDeployConn);\n }\n }\n\n get defaultMaxWorkers () {\n return portalWorker.supervisor.maxWorkingSandboxes\n }\n\n set defaultMaxWorkers (value) {\n portalWorker.supervisor.maxWorkingSandboxes = value\n }\n\n get paymentAddress () {\n return portalWorker.supervisor.paymentAddress\n }\n\n set paymentAddress (value) {\n portalWorker.supervisor.paymentAddress = value\n }\n\n get mining () {\n return portalWorker.working\n }\n\n /** Return a special object that represents the current market value\n * @param {number} factor - OPTIONAL multiplier. default: 1.0\n */\n marketValue (factor = 1) {\n return {\n [MARKET_VALUE]: factor\n };\n }\n\n /** Describes the payment required to compute such a slice on a worker\n * or in a market working at the rates described in the WorkValue object.\n * This function does not take into account job-related overhead.\n * \n * @param {object} sliceProfile A SliceProfile-shaped object\n * @param {object} workValue A WorkValue-shaped object\n * @returns {number} Payment value in DCC describing rate to compute slice\n */\n calculateSlicePayment(sliceProfile, workValue) {\n return sliceProfile.cpuHours * workValue.cpuHour\n + sliceProfile.gpuHours * workValue.gpuHour\n + sliceProfile.inputMBytes * workValue.inputMByte\n + sliceProfile.outputMBytes * workValue.outputMByte;\n }\n\n \n /**\n * Deprecated `mine` API. Use `work` instead\n */\n async mine (numberOfCores, paymentAddress = this.paymentAddress) {\n return this.work(numberOfCores, paymentAddress);\n }\n\n /**\n * Instruct the Worker to stop working, then emit workerInactive\n */\n async stopWorking() {\n console.warn('Using the Compute API for controlling workers is deprecated. Use the Worker API instead.')\n await portalWorker.stop(!this.allowSlicesToFinishBeforeStopping)\n }\n\n /**\n * Deprecated `stopMining` API. Use `stopWorking` instead\n */\n stopMining () {\n console.log(\"Warning: used deprecated 'stopMining' API\");\n this.stopWorking();\n }\n \n /**\n * Flag worker as disabled\n */\n disableWorker() {\n disableWorker();\n }\n\n /**\n * Deprecated `disableMiner` API. Use `disableWorker` instead\n */\n disableMiner() {\n this.disableWorker();\n }\n\n /**\n * Experimental replacement for mine() /* wg aug 2019\n * @param {number} [numberOfCores = this.defaultMaxWorkers] - Number of parallel processes to run\n * @param {string} [paymentAddress = this.paymentAddress || wallet.get().address] - The address of the account to work for\n */\n async work(numCores, paymentAddress) {\n console.warn('Using the Compute API for controlling workers is deprecated. Use the Worker API instead.');\n\n portalWorker.supervisor.paymentAddress = paymentAddress || this.paymentAddress || (await wallet.get()).address;\n portalWorker.supervisor.maxWorkingSandboxes = numCores || this.defaultMaxWorkers || 1;\n await portalWorker.start();\n\n return portalWorker;\n }\n\n /**\n * Form 1. Create a Job for the distributed computer by specifying the start,\n * end, and step for a RangeObject with a given work function and arguments.\n *\n * @param {number} start First number in a range (see {@link RangeObject})\n * @param {number} end Last number in a range (see {@link RangeObject})\n * @param {number} [step=1] the space between numbers in a range (see\n * {@link RangeObject})\n * @param {function | string | URL | DcpURL} work Work function as string or\n * function literal.\n * @param {object} [arguments=[]] optional Array-like object which contains\n * arguments that are passed to the work function\n * @returns {Job}\n * @access public\n * @example\n * let job = compute.for(1, 3, function (i) {\n * progress('100%')\n * return i*10\n * })\n */\n /**\n * Form 2. Create a Job for the distributed computer by specifying the input\n * as a RemoteDataSet with a given work function and arguments.\n *\n * @param {RemoteDataSet} remoteDataSet Array, ES6 function* generator, or any\n * other type of iterable object.\n * @param {function | string | URL | DcpURL} work Work function as string or\n * function literal.\n * @param {object} [arguments=[]] optional Array-like object which contains\n * arguments that are passed to the work function\n * @returns {Job}\n * @access public\n * @example\n * const remoteData = ['https://example.com/data'];\n * const job = compute.for(new RemoteDataSet(...remoteData), function(i) {\n * progress(1)\n * return i\n * })\n */\n /**\n * Form 3. Create a Job for the distributed computer by specifying the input\n * as an Iterable object with a given work function and arguments.\n *\n * @param {Iterable} iterableObject Array, ES6 function* generator, or any\n * other type of iterable object.\n * @param {function | string | URL | DcpURL} work Work function as string or\n * function literal.\n * @param {object} [arguments=[]] optional Array-like object which contains\n * arguments that are passed to the work function\n * @returns {Job}\n * @access public\n * @example\n * let job = compute.for([123,456], function(i) {\n * progress(1)\n * return i\n * })\n */\n /**\n * Form 4. Create a Job for the distributed computer by specifying the input\n * as a RangeObject with a given work function and arguments.\n *\n * @param {RangeObject} rangeObject Either a RangeObject or an object literal\n * which can be used to create a RangeObject (see {@link RangeObject}).\n * @param {function | string | URL | DcpURL} work Work function as string or\n * function literal.\n * @param {object} [arguments=[]] optional Array-like object which contains\n * arguments that are passed to the work function\n * @returns {Job}\n * @access public\n * @example\n * let job = compute.for({start: 10, end: 13, step: 2}, (i) => progress(1) && i)\n */\n /**\n * Form 5. Similar to Form 4, except with a multi range object containing an\n * array of range objects in the key ranges. These are used to create\n * multi-dimensional ranges, like nested loops. If they were written as\n * traditional loops, the outermost loop would be the leftmost range object,\n * and the innermost loop would be the rightmost range object.\n *\n * @param {MultiRangeObject} multiRange Either a MultiRangeObject or any valid\n * argument to the constructor.\n * @param {function | string | URL | DcpURL} work Work function as string or\n * function literal.\n * @param {object} [arguments=[]] optional Array-like object which contains\n * arguments that are passed to the work function\n * @returns {Job}\n * @access public\n * @example\n * let job = compute.for([{start: 1, end: 2}, {start: 3, end: 5}], function(i,j) {\n * return [i, j, i*j]\n * })\n */\n for(...args) {\n // args, or work function\n const lastArg = args[args.length - 1];\n // work function, if args provided\n const secondLastArg = args[args.length - 2];\n let range = null;\n let work;\n let genArgs;\n\n // All forms: extract work and optional args from the end and validate it.\n if (isValidWorkFunctionType(secondLastArg)) {\n // for(..., workFunction, arguments)\n work = secondLastArg\n if (Array.from(lastArg).length > 0 || typeof lastArg === 'string') {\n // array or string, use as-is (string should be a URI)\n genArgs = lastArg;\n } else if (lastArg === undefined) {\n genArgs = [];\n } else {\n throw new TypeError(\n 'Work function arguments (the last parameter), should be an array.',\n );\n }\n } else if (isValidWorkFunctionType(lastArg)) {\n // for(..., workFunction)\n work = lastArg;\n genArgs = [];\n }\n\n if (!isWorkFunctionValid(work)) {\n throw new TypeError(\n `Work parameter must be a function or a string that evaluates to a function, or a URL that points to the location of a function.\\nReceived type ${typeof work} (${work})`,\n );\n }\n\n // Extract the proper information from every form.\n const [firstArg, secondArg, thirdArg] = args;\n if (['object', 'string'].includes(typeof firstArg)) {\n // Forms 2 & 3 (data is iterable, eg. an Array or RemoteDataSet)\n // Forms 4 & 5 (data is RangeObject & Friends)\n range = rehydrateRange(firstArg);\n } else if (typeof firstArg === 'number' && typeof secondArg === 'number') {\n // Form 1 (data is start, end[, step])\n const start = firstArg;\n const end = secondArg;\n\n /**\n * If there is an argument between the end and the work fn, use it as step\n * otherwise set step to undefined.\n */\n const step = typeof thirdArg === 'number' ? thirdArg : undefined;\n range = new RangeObject(start, end, step);\n } else {\n throw new TypeError(\n 'Parameters do not match one of the accepted call forms.',\n );\n }\n\n if (range.length === 0) {\n throw new Error('Length of input set must be greater than zero.');\n }\n\n debug('Work:', work);\n debug('Input Set:', range);\n debug('Arguments:', genArgs);\n return new Job(work, range, genArgs);\n\n /**\n * @param {any} workFunction\n */\n function isValidWorkFunctionType(workFunction) {\n return (\n typeof workFunction === 'function' ||\n typeof workFunction === 'string' ||\n DcpURL.isURL(workFunction)\n );\n }\n\n /**\n * Checks if the workFunction can be evaluated to a function or if it's\n * protocol is HTTPS,\n * @param {function | string | URL | DcpURL} workFunction\n */\n function isWorkFunctionValid(workFunction) {\n switch (typeof workFunction) {\n case 'function':\n return true;\n case 'string': {\n return isValidFunction(workFunction);\n }\n case 'object':\n return DcpURL.isURL(workFunction);\n default:\n return false;\n }\n }\n\n /**\n * Evaluates to true if the string can be coerced into a javascript\n * function.\n * @param {string} workFunction\n */\n function isValidFunction(workFunction) {\n try {\n const fn = indirectEval(`(${workFunction})`);\n return typeof fn === 'function';\n } catch (e) {\n return false;\n }\n }\n }\n\n /**\n * Form 1.\n * Create a job which will run a work function once\n *\n * @param {function | string | URL | DcpURL} work Work function as string or function literal.\n * @param {object} [workFunctionArgs=[]] optional Array-like object which contains arguments which are passed to the work func\n * @returns {Job}\n * @access public\n * @example\n * const job = compute.do(function (i) {\n * progress('100%')\n * return i*10\n * })\n */\n /**\n * Form 2.\n * Create a job which will run a work function on the values 1..n\n *\n * @param {number} n Number of times to run the work function.\n * @param {function | string | URL | DcpURL} work Work function as string or function literal.\n * @param {object} [workFunctionArgs=[]] optional Array-like object which contains arguments which are passed to the work func\n * @returns {Job}\n * @access public\n * @example\n * const job = compute.do(5, function (i) {\n * progress('100%')\n * return i*10\n * })\n */\n do(...args) {\n let n;\n let work;\n let workFunctionArgs;\n if (typeof args[0] !== 'number') {\n // Form 1.\n n = 1;\n [work, workFunctionArgs] = args;\n } else {\n // Form 2.\n [n, work, workFunctionArgs] = args;\n }\n return this.for(0, n - 1, work, workFunctionArgs);\n }\n\n /** Cancel a running job, by opaque id\n *\n * @param {string} id Address of the job to cancel.\n * @param {wallet.Keystore} ownerKeystore The keystore of the job owner\n * @param {string} reason If provided, will be sent on to client\n * @returns {object} The status of the funds that were released from escrow.\n * @throws when id or ownerKeystore are not provided.\n * @access public\n */\n async cancel (id, ownerKeystore, reason = '') {\n if (typeof id !== 'string') {\n throw new Error(`compute.cancel: Job ID must be a string, not ${typeof id}.`);\n }\n\n if (!(ownerKeystore instanceof wallet.Keystore)) {\n throw new Error('compute.cancel: ownerKeystore must be instance of wallet.Keystore');\n }\n\n if (!this.deployConnection)\n this.openDeployConn();\n\n const response = await this.deployConnection.send('cancelJob', {\n job: id,\n ownerAddress: ownerKeystore.address,\n reason,\n }, ownerKeystore);\n\n return response.payload;\n }\n\n /**\n * Reconnect/resume a job, by opaque id\n *\n * @param {string} id Opaque id of the Job to resume/reconnect.\n * @param {wallet.Keystore} [ownerKeystore=wallet.get()] The keystore of the job owner\n * @returns {Job}\n * @throws when id is not provided.\n * @access public\n */\n async resume (id, ownerKeystore) {\n if (typeof id !== 'string') {\n throw new Error(`compute.resume: Job ID must be a string, not ${typeof id}.`);\n }\n\n if (!(ownerKeystore instanceof wallet.Keystore)) {\n ownerKeystore = await wallet.get();\n }\n\n const response = await this.schedulerConnection.send('fetchJob', {\n job: id, // XXX@TODO change 'address' to 'job' once we port this operation to v4\n owner: ownerKeystore.address,\n }, ownerKeystore);\n\n const job = new Job(response.payload);\n job.setPaymentAccountKeystore(ownerKeystore);\n return job;\n }\n\n /**\n * Form 1. Query the status of a job that has been deployed to the scheduler.\n *\n * @access public\n * @param {string|Job} job A job ID or a Job instance\n * @param {wallet.Keystore} ownerKeystore The identity keystore of the owner\n * of the job\n * @throws when ownerKeystore is not provided\n * @returns {Promise<object>} A status object describing a given job.\n */\n /**\n * Form 2. Query the status of jobs deployed to the scheduler.\n *\n * @access public\n * @param {object} [opts] Query options\n * @param {number} [opts.startTime] Jobs older than this will be excluded from\n * the results.\n * @param {number} [opts.endTime] Jobs newer than this will be excluded from\n * the results.\n * @param {wallet.Keystore} ownerKeystore The identity keystore of the owner\n * of the jobs to query\n * @throws when ownerKeystore is not provided\n * @returns {Promise<Array<Object>>} An array of status objects corresponding to the status of\n * jobs deployed by the given payment account.\n */\n async status(...args) {\n if (args.length !== 2) {\n throw new Error('Only 2 arguments must be passed to compute.status');\n }\n\n const ownerKeystore = args.pop();\n if (!(ownerKeystore instanceof wallet.Keystore)) {\n throw new TypeError(\n 'compute.status: ownerKeystore must be an instance of Keystore',\n );\n }\n\n const requestPayload = {\n idKeystoreOwner: ownerKeystore.address,\n };\n\n const firstArg = args.shift();\n if (typeof firstArg === 'string' || firstArg instanceof Job) {\n // Form 1\n requestPayload.jobId = firstArg instanceof Job ? firstArg.id : firstArg;\n } else if (typeof firstArg === 'object') {\n // Form 2\n if (\n (typeof firstArg.startTime !== 'undefined' &&\n !(firstArg.startTime instanceof Date)) ||\n (typeof firstArg.endTime !== 'undefined' &&\n !(firstArg.endTime instanceof Date))\n ) {\n throw new TypeError(\n 'startTime and or endTime were not specified as instances of Date',\n );\n }\n\n requestPayload.startDeployTime = firstArg.startTime;\n requestPayload.endDeployTime = firstArg.endTime;\n } else {\n throw new TypeError(`Unknown first argument type ${typeof firstArg}`);\n }\n\n if (!this.deployConnection)\n this.openDeployConn();\n\n const {\n success,\n payload: responsePayload,\n } = await this.deployConnection.send(\n 'listJobs',\n requestPayload,\n ownerKeystore,\n );\n\n if (!success) {\n throw new DCPError('Request for job status failed', responsePayload);\n }\n\n if (typeof firstArg === 'string' || firstArg instanceof Job) {\n return responsePayload[0];\n }\n\n return responsePayload;\n }\n\n /**\n * Returns information for the job with the provided ID. It will use\n * `wallet.getId()` to retrieve the auth keystore.\n *\n * @param {string} jobID The job ID\n * @returns {Promise<object>} status object describing a given job\n * @access public\n */\n async getJobInfo(jobID) {\n if (!this.phemeConnection)\n this.openPhemeConn();\n\n const ks = await wallet.getId();\n const { payload: jobResponsePayload } = await this.phemeConnection.send('fetchJobReport', \n {\n jobOwner: ks.address,\n job: jobID\n });\n\n const { payload: sliceResponsePayload } = await this.phemeConnection.send(\n 'fetchSliceReport', \n {\n jobOwner: ks.address,\n job: jobID\n },\n );\n\n const jobReport = {};\n Object.assign(jobReport, jobResponsePayload, sliceResponsePayload);\n\n return jobReport;\n }\n\n /**\n * Returns information for the slices of the job with the provided ID. It will\n * use `wallet.getId()` to retrieve the auth keystore.\n *\n * @param {string} jobId The job ID\n * @returns {Promise<object>} A slice status object for the given job.\n * @access public\n */\n async getSliceInfo(jobId) {\n if (!this.phemeConnection)\n this.openPhemeConn();\n\n const identityKeystore = await wallet.getId();\n const {\n success,\n payload: responsePayload,\n } = await this.phemeConnection.send(\n 'fetchSliceReport',\n {\n job: jobId,\n jobOwner: identityKeystore.address,\n },\n identityKeystore,\n );\n\n if (!success) {\n throw new DCPError('Request for slice info failed', responsePayload);\n }\n\n return responsePayload;\n }\n}\n\nexports.RemoteDataSet = RemoteDataSet;\nexports.MARKET_VALUE = MARKET_VALUE;\nexports.compute = new Compute(); /* :( */\nexports.compute.getPublicComputeGroupId = () => __webpack_require__(/*! dcp/common/scheduler-constants */ \"./src/common/scheduler-constants.js\").computeGroups.public.id;\nexports.compute.getPublicComputeGroupDescriptor = () => __webpack_require__(/*! dcp/common/scheduler-constants */ \"./src/common/scheduler-constants.js\").computeGroups.public;\nexports.compute.ResultHandle = ResultHandle; // export as compute.ResultHandle for instanceof checking\nexports.version = {\n api: '1.5.2',\n provides: '1.0.0' /* dcpConfig.scheduler.compatibility.operations.compute */\n};\n\n\n//# sourceURL=webpack:///./src/dcp-client/compute.js?");
5079
+ eval("/**\n * @file Module that implements Compute API\n * @module dcp/compute\n * @access public\n */\n\n/* global dcpConfig */\n\n/**\n * @access private - Will change soon!\n * @typedef {object} JobRequirements\n * @property {object} environment\n * @property {boolean} environment.fdlibm - Workers express capability 'fdlibm' in order for client applications to have confidence that results will be bitwise identical across workers. This library is recommended, but not required, for implementaiton of Google chrome, Mozilla Firefox and the DCP standalone worker use fdlibm but Microsoft Edge and Apple Safari do not use it.\n * @property {boolean} environment.offscreenCanvas When present, this capability indicates that the worker environment has the OffscreenCanvas symbol defined.\n * @property {object} details\n * @property {object} details.offscreenCanvas\n * @property {boolean} details.offscreenCanvas.bigTexture4096 This capability indicates that the worker's WebGL MAX_TEXTURE_SIZE is at least 4096.\n * @property {boolean} details.offscreenCanvas.bigTexture8192 This capability indicates that the worker's WebGL MAX_TEXTURE_SIZE is at least 8192.\n * @property {boolean} details.offscreenCanvas.bigTexture16384 This capability indicates that the worker's WebGL MAX_TEXTURE_SIZE is at least 16384.\n * @property {boolean} details.offscreenCanvas.bigTexture32768 This capability indicates that the worker's WebGL MAX_TEXTURE_SIZE is at least 32768.\n * @property {object} engine\n * @property {boolean} engine.es7 This capability enforces that the worker is running an es7-compliant JavaScript engine.\n * @property {boolean} engine.spidermonkey The capability enforces that the worker will run in the SpiderMonkey JS engine.\n * @property {boolean} browser.chrome The capability to exclude Chrome browsers.\n */\n\n/**\n * This JSDoc 'namespace' is a convenient way to document some globals that are available only in\n * the sandbox environment provided to work functions.\n * @access public\n * @namespace sandboxEnv\n */\n\n/**\n * This function emits a progress event. Progress events should be emitted approximately once per second; a task which fails to emit a \n * progress event for a certain period of time will be cancelled by the supervisor. The argument to this function is interpreted to six \n * significant digits, and must increase for every call. *All work functions must emit at least one progress event* - this requirement \n * will be enforced by the estimator. * The period of time mentioned above will be at least 30 wall-clock seconds and at least 30 \n * benchmark-adjusted seconds\n * @access public\n * @method progress\n * @param {string|number|undefined} n a number between 0 and 1 (inclusive) that represents a best-guess \n * at the completed portion of the task as a ratio of completed work to total work. If the argument is a string ending in the `%` symbol, \n * it will be interpreted as a number in the usual mathematical sense. If the argument is `undefined`, the slice progress is noted but \n * the amount of progress is considered to be indeterminate.\n * @memberof module:dcp/compute~sandboxEnv\n * @returns {boolean} true\n */\n\n/**\n * Emit a `console` event to the `JobHandle` in the client. If the sandbox is running in an environment with a native console object, the native method may also be invoked.\n * \n * @member console\n * @property {function} log fire console event with 'log' as `level` (see {@link Job#event:console|Job#event:console.level}).\n * @property {function} debug fire console event with 'debug' as `level` (see {@link Job#event:console|Job#event:console.level}).\n * @property {function} info fire console event with 'info' as `level` (see {@link Job#event:console|Job#event:console.level}).\n * @property {function} warn fire console event with 'warn' as `level` (see {@link Job#event:console|Job#event:console.level}).\n * @property {function} error fire console event with 'error' as `level` (see {@link Job#event:console|Job#event:console.level}).\n * \n * **Note** some ES environments (Chrome, Firefox) implement C-style print formatting in this method. This is currently not supported.\n * @memberof module:dcp/compute~sandboxEnv\n * @emits Job#console\n * @access public\n */\n\n/**\n * @member work An object that contains the following properties\n * @property {function} emit (eventName, ...) - send arbitrary args with `eventName`. This event is received client-side with those args (see {@link Job#work}).\n * @property {object} job\n * @property {object} job.public Property set on job handle, see {@link Job#public}.\n * @memberof module:dcp/compute~sandboxEnv\n * @access public\n */\n\n/**\n * @member {OffscreenCanvas} OffscreenCanvas As defined in the {@link https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvas|HTML Standard}, provides a canvas which can be rendered off-screen. If this interface is not available in a given worker, the worker will not report capability \"offscreenCanvas\".\n * @memberof module:dcp/compute~sandboxEnv\n * @access public\n */\n\nconst protocolV4 = __webpack_require__(/*! dcp/protocol-v4 */ \"./src/protocol-v4/index.js\");\nconst { EventEmitter } = __webpack_require__(/*! dcp/common/dcp-events/event-emitter */ \"./src/common/dcp-events/event-emitter.js\");\nconst { RangeObject, rehydrateRange } = __webpack_require__(/*! dcp/dcp-client/range-object */ \"./src/dcp-client/range-object.js\");\nconst { RemoteDataSet } = __webpack_require__(/*! dcp/dcp-client/remote-data-set */ \"./src/dcp-client/remote-data-set.js\");\nconst { RemoteDataPattern } = __webpack_require__(/*! dcp/dcp-client/remote-data-pattern */ \"./src/dcp-client/remote-data-pattern.js\");\nconst { Job, ResultHandle } = __webpack_require__(/*! dcp/dcp-client/job */ \"./src/dcp-client/job/index.js\");\nconst wallet = __webpack_require__(/*! dcp/dcp-client/wallet */ \"./src/dcp-client/wallet/index.js\");\nconst { DcpURL } = __webpack_require__(/*! dcp/common/dcp-url */ \"./src/common/dcp-url.js\");\nconst { disableWorker } = __webpack_require__(/*! dcp/dcp-client/worker */ \"./src/dcp-client/worker/index.js\");\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\nconst debugging = __webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope('dcp-client');\n\nconst debug = (...args) => {\n if (debugging()) {\n args.unshift('dcp-client:compute');\n console.debug(...args);\n }\n};\n\nconst MARKET_VALUE = Symbol('Market Value');\nconst indirectEval = eval; // eslint-disable-line no-eval\n\n/**\n * Instance methods of this class are exported as static members of {@link module:dcp/compute|dcp/compute}.\n * @example\n * let job = compute.for(1, 3, function (i) {\n * progress('100%');\n * return i*10;\n * })\n * let results = await job.exec();\n */\nclass Compute extends EventEmitter {\n constructor () {\n super('Compute');\n\n /** @todo move these into some central config */\n this.defaultMaxGPUs = 1;\n this.GPUsAssigned = 0;\n this.allowSlicesToFinishBeforeStopping = false;\n\n this.schedulerURL = undefined; // Will default to dcpConfig.scheduler.location if not overidden\n this.bankURL = undefined; // Will default to dcpConfig.bank.services.bankTeller.location if not overidden\n\n Object.defineProperty(this.marketValue, MARKET_VALUE, {\n value: true,\n writable: false,\n configurable: false,\n enumerable: false,\n });\n\n const ceci = this;\n this.RemoteDataSet = RemoteDataSet;\n this.RemoteDataPattern = RemoteDataPattern;\n // Initialize to null so these properties are recognized for the Job class\n this.phemeConnection = null;\n this.deployConnection = null;\n this.openPhemeConn = function openBankConn()\n {\n ceci.phemeConnection = new protocolV4.Connection(dcpConfig.scheduler.services.pheme);\n ceci.phemeConnection.on('close', ceci.openPhemeConn);\n }\n\n this.openDeployConn = function openDeployConn()\n {\n ceci.deployConnection = new protocolV4.Connection(dcpConfig.scheduler.services.jobSubmit);\n ceci.deployConnection.on('close', ceci.openDeployConn);\n }\n }\n\n get defaultMaxWorkers () {\n return portalWorker.supervisor.maxWorkingSandboxes\n }\n\n set defaultMaxWorkers (value) {\n portalWorker.supervisor.maxWorkingSandboxes = value\n }\n\n get paymentAddress () {\n return portalWorker.supervisor.paymentAddress\n }\n\n set paymentAddress (value) {\n portalWorker.supervisor.paymentAddress = value\n }\n\n get mining () {\n return portalWorker.working\n }\n\n /** Return a special object that represents the current market value\n * @param {number} factor - OPTIONAL multiplier. default: 1.0\n */\n marketValue (factor = 1) {\n return {\n [MARKET_VALUE]: factor\n };\n }\n\n /** Describes the payment required to compute such a slice on a worker\n * or in a market working at the rates described in the WorkValue object.\n * This function does not take into account job-related overhead.\n * \n * @param {object} sliceProfile A SliceProfile-shaped object\n * @param {object} workValue A WorkValue-shaped object\n * @returns {number} Payment value in DCC describing rate to compute slice\n */\n calculateSlicePayment(sliceProfile, workValue) {\n return sliceProfile.cpuHours * workValue.cpuHour\n + sliceProfile.gpuHours * workValue.gpuHour\n + sliceProfile.inputMBytes * workValue.inputMByte\n + sliceProfile.outputMBytes * workValue.outputMByte;\n }\n\n \n /**\n * Deprecated `mine` API. Use `work` instead\n */\n async mine (numberOfCores, paymentAddress = this.paymentAddress) {\n return this.work(numberOfCores, paymentAddress);\n }\n\n /**\n * Instruct the Worker to stop working, then emit workerInactive\n */\n async stopWorking() {\n console.warn('Using the Compute API for controlling workers is deprecated. Use the Worker API instead.')\n await portalWorker.stop(!this.allowSlicesToFinishBeforeStopping)\n }\n\n /**\n * Deprecated `stopMining` API. Use `stopWorking` instead\n */\n stopMining () {\n console.log(\"Warning: used deprecated 'stopMining' API\");\n this.stopWorking();\n }\n \n /**\n * Flag worker as disabled\n */\n disableWorker() {\n disableWorker();\n }\n\n /**\n * Deprecated `disableMiner` API. Use `disableWorker` instead\n */\n disableMiner() {\n this.disableWorker();\n }\n\n /**\n * Experimental replacement for mine() /* wg aug 2019\n * @param {number} [numberOfCores = this.defaultMaxWorkers] - Number of parallel processes to run\n * @param {string} [paymentAddress = this.paymentAddress || wallet.get().address] - The address of the account to work for\n */\n async work(numCores, paymentAddress) {\n console.warn('Using the Compute API for controlling workers is deprecated. Use the Worker API instead.');\n\n portalWorker.supervisor.paymentAddress = paymentAddress || this.paymentAddress || (await wallet.get()).address;\n portalWorker.supervisor.maxWorkingSandboxes = numCores || this.defaultMaxWorkers || 1;\n await portalWorker.start();\n\n return portalWorker;\n }\n\n /**\n * Form 1. Create a Job for the distributed computer by specifying the start,\n * end, and step for a RangeObject with a given work function and arguments.\n *\n * @param {number} start First number in a range (see {@link RangeObject})\n * @param {number} end Last number in a range (see {@link RangeObject})\n * @param {number} [step=1] the space between numbers in a range (see\n * {@link RangeObject})\n * @param {function | string | URL | DcpURL} work Work function as string or\n * function literal.\n * @param {object} [arguments=[]] optional Array-like object which contains\n * arguments that are passed to the work function\n * @returns {Job}\n * @access public\n * @example\n * let job = compute.for(1, 3, function (i) {\n * progress('100%')\n * return i*10\n * })\n */\n /**\n * Form 2.a Create a Job for the distributed computer by specifying the input\n * as a RemoteDataSet with a given work function and arguments.\n *\n * @param {RemoteDataSet} remoteDataSet Array, ES6 function* generator, or any\n * other type of iterable object.\n * \n * Form 2.b Create a Job for the distributed computer by specifying the input\n * as a RemoteDataPattern with a given work function and arguments.\n * @param {RemoteDataPattern} RemoteDataPattern Array, ES6 function* generator, or any\n * other type of iterable object.\n * @param {function | string | URL | DcpURL} work Work function as string or\n * function literal.\n * @param {object} [arguments=[]] optional Array-like object which contains\n * arguments that are passed to the work function\n * @returns {Job}\n * @access public\n * @example\n * const remoteData = ['https://example.com/data'];\n * const job = compute.for(new RemoteDataSet(...remoteData), function(i) {\n * progress(1)\n * return i\n * })\n * \n * * @example\n * const pattern = 'https://example.com/data/{slice}.json';\n * const NumberOfSlices = 5;\n * const job = compute.for(new RemoteDataSet(pattern, NumberOfSlices), function(i) {\n * progress(1)\n * return i\n * })\n */\n /**\n * Form 3. Create a Job for the distributed computer by specifying the input\n * as an Iterable object with a given work function and arguments.\n *\n * @param {Iterable} iterableObject Array, ES6 function* generator, or any\n * other type of iterable object.\n * @param {function | string | URL | DcpURL} work Work function as string or\n * function literal.\n * @param {object} [arguments=[]] optional Array-like object which contains\n * arguments that are passed to the work function\n * @returns {Job}\n * @access public\n * @example\n * let job = compute.for([123,456], function(i) {\n * progress(1)\n * return i\n * })\n */\n /**\n * Form 4. Create a Job for the distributed computer by specifying the input\n * as a RangeObject with a given work function and arguments.\n *\n * @param {RangeObject} rangeObject Either a RangeObject or an object literal\n * which can be used to create a RangeObject (see {@link RangeObject}).\n * @param {function | string | URL | DcpURL} work Work function as string or\n * function literal.\n * @param {object} [arguments=[]] optional Array-like object which contains\n * arguments that are passed to the work function\n * @returns {Job}\n * @access public\n * @example\n * let job = compute.for({start: 10, end: 13, step: 2}, (i) => progress(1) && i)\n */\n /**\n * Form 5. Similar to Form 4, except with a multi range object containing an\n * array of range objects in the key ranges. These are used to create\n * multi-dimensional ranges, like nested loops. If they were written as\n * traditional loops, the outermost loop would be the leftmost range object,\n * and the innermost loop would be the rightmost range object.\n *\n * @param {MultiRangeObject} multiRange Either a MultiRangeObject or any valid\n * argument to the constructor.\n * @param {function | string | URL | DcpURL} work Work function as string or\n * function literal.\n * @param {object} [arguments=[]] optional Array-like object which contains\n * arguments that are passed to the work function\n * @returns {Job}\n * @access public\n * @example\n * let job = compute.for([{start: 1, end: 2}, {start: 3, end: 5}], function(i,j) {\n * return [i, j, i*j]\n * })\n */\n for(...args) {\n // args, or work function\n const lastArg = args[args.length - 1];\n // work function, if args provided\n const secondLastArg = args[args.length - 2];\n let range = null;\n let work;\n let genArgs;\n\n // All forms: extract work and optional args from the end and validate it.\n if (isValidWorkFunctionType(secondLastArg)) {\n // for(..., workFunction, arguments)\n work = secondLastArg\n if (lastArg && Array.from(lastArg).length > 0 || typeof lastArg === 'string') {\n // array or string, use as-is (string should be a URI)\n genArgs = lastArg;\n } else if (lastArg === undefined) {\n genArgs = [];\n } else {\n throw new TypeError(\n 'Work function arguments (the last parameter), should be an array.',\n );\n }\n } else if (isValidWorkFunctionType(lastArg)) {\n // for(..., workFunction)\n work = lastArg;\n genArgs = [];\n }\n\n if (!isWorkFunctionValid(work)) {\n throw new TypeError(\n `Work parameter must be a function or a string that evaluates to a function, or a URL that points to the location of a function.\\nReceived type ${typeof work} (${work})`,\n );\n }\n\n // Extract the proper information from every form.\n const [firstArg, secondArg, thirdArg] = args;\n if (['object', 'string'].includes(typeof firstArg)) {\n // Forms 2 & 3 (data is iterable, eg. an Array or RemoteDataSet)\n // Forms 4 & 5 (data is RangeObject & Friends)\n range = rehydrateRange(firstArg);\n } else if (typeof firstArg === 'number' && typeof secondArg === 'number') {\n // Form 1 (data is start, end[, step])\n const start = firstArg;\n const end = secondArg;\n\n /**\n * If there is an argument between the end and the work fn, use it as step\n * otherwise set step to undefined.\n */\n const step = typeof thirdArg === 'number' ? thirdArg : undefined;\n range = new RangeObject(start, end, step);\n } else {\n throw new TypeError(\n 'Parameters do not match one of the accepted call forms.',\n );\n }\n\n if (range.length === 0) {\n throw new Error('Length of input set must be greater than zero.');\n }\n\n debug('Work:', work);\n debug('Input Set:', range);\n debug('Arguments:', genArgs);\n return new Job(work, range, genArgs);\n\n /**\n * @param {any} workFunction\n */\n function isValidWorkFunctionType(workFunction) {\n return (\n typeof workFunction === 'function' ||\n typeof workFunction === 'string' ||\n DcpURL.isURL(workFunction)\n );\n }\n\n /**\n * Checks if the workFunction can be evaluated to a function or if it's\n * protocol is HTTPS,\n * @param {function | string | URL | DcpURL} workFunction\n */\n function isWorkFunctionValid(workFunction) {\n switch (typeof workFunction) {\n case 'function':\n return true;\n case 'string': {\n return isValidFunction(workFunction);\n }\n case 'object':\n return DcpURL.isURL(workFunction);\n default:\n return false;\n }\n }\n\n /**\n * Evaluates to true if the string can be coerced into a javascript\n * function.\n * @param {string} workFunction\n */\n function isValidFunction(workFunction) {\n try {\n const fn = indirectEval(`(${workFunction})`);\n return typeof fn === 'function';\n } catch (e) {\n return false;\n }\n }\n }\n\n /**\n * Form 1.\n * Create a job which will run a work function once\n *\n * @param {function | string | URL | DcpURL} work Work function as string or function literal.\n * @param {object} [workFunctionArgs=[]] optional Array-like object which contains arguments which are passed to the work func\n * @returns {Job}\n * @access public\n * @example\n * const job = compute.do(function (i) {\n * progress('100%')\n * return i*10\n * })\n */\n /**\n * Form 2.\n * Create a job which will run a work function on the values 1..n\n *\n * @param {number} n Number of times to run the work function.\n * @param {function | string | URL | DcpURL} work Work function as string or function literal.\n * @param {object} [workFunctionArgs=[]] optional Array-like object which contains arguments which are passed to the work func\n * @returns {Job}\n * @access public\n * @example\n * const job = compute.do(5, function (i) {\n * progress('100%')\n * return i*10\n * })\n */\n do(...args) {\n let n;\n let work;\n let workFunctionArgs;\n if (typeof args[0] !== 'number') {\n // Form 1.\n n = 1;\n [work, workFunctionArgs] = args;\n } else {\n // Form 2.\n [n, work, workFunctionArgs] = args;\n }\n return this.for(0, n - 1, work, workFunctionArgs);\n }\n\n /** Cancel a running job, by opaque id\n *\n * @param {string} id Address of the job to cancel.\n * @param {wallet.Keystore} ownerKeystore The keystore of the job owner\n * @param {string} reason If provided, will be sent on to client\n * @returns {object} The status of the funds that were released from escrow.\n * @throws when id or ownerKeystore are not provided.\n * @access public\n */\n async cancel (id, ownerKeystore, reason = '') {\n if (typeof id !== 'string') {\n throw new Error(`compute.cancel: Job ID must be a string, not ${typeof id}.`);\n }\n\n if (!(ownerKeystore instanceof wallet.Keystore)) {\n throw new Error('compute.cancel: ownerKeystore must be instance of wallet.Keystore');\n }\n\n if (!this.deployConnection)\n this.openDeployConn();\n\n const response = await this.deployConnection.send('cancelJob', {\n job: id,\n ownerAddress: ownerKeystore.address,\n reason,\n }, ownerKeystore);\n\n return response.payload;\n }\n\n /**\n * Reconnect/resume a job, by opaque id\n *\n * @param {string} id Opaque id of the Job to resume/reconnect.\n * @param {wallet.Keystore} [ownerKeystore=wallet.get()] The keystore of the job owner\n * @returns {Job}\n * @throws when id is not provided.\n * @access public\n */\n async resume (id, ownerKeystore) {\n if (typeof id !== 'string') {\n throw new Error(`compute.resume: Job ID must be a string, not ${typeof id}.`);\n }\n\n if (!(ownerKeystore instanceof wallet.Keystore)) {\n ownerKeystore = await wallet.get();\n }\n\n const response = await this.schedulerConnection.send('fetchJob', {\n job: id, // XXX@TODO change 'address' to 'job' once we port this operation to v4\n owner: ownerKeystore.address,\n }, ownerKeystore);\n\n const job = new Job(response.payload);\n job.setPaymentAccountKeystore(ownerKeystore);\n return job;\n }\n\n /**\n * Form 1. Query the status of a job that has been deployed to the scheduler.\n *\n * @access public\n * @param {string|Job} job A job ID or a Job instance\n * @param {wallet.Keystore} ownerKeystore The identity keystore of the owner\n * of the job\n * @throws when ownerKeystore is not provided\n * @returns {Promise<object>} A status object describing a given job.\n */\n /**\n * Form 2. Query the status of jobs deployed to the scheduler.\n *\n * @access public\n * @param {object} [opts] Query options\n * @param {number} [opts.startTime] Jobs older than this will be excluded from\n * the results.\n * @param {number} [opts.endTime] Jobs newer than this will be excluded from\n * the results.\n * @param {wallet.Keystore} ownerKeystore The identity keystore of the owner\n * of the jobs to query\n * @throws when ownerKeystore is not provided\n * @returns {Promise<Array<Object>>} An array of status objects corresponding to the status of\n * jobs deployed by the given payment account.\n */\n async status(...args) {\n if (args.length !== 2) {\n throw new Error('Only 2 arguments must be passed to compute.status');\n }\n\n const ownerKeystore = args.pop();\n if (!(ownerKeystore instanceof wallet.Keystore)) {\n throw new TypeError(\n 'compute.status: ownerKeystore must be an instance of Keystore',\n );\n }\n\n const requestPayload = {\n idKeystoreOwner: ownerKeystore.address,\n };\n\n const firstArg = args.shift();\n if (typeof firstArg === 'string' || firstArg instanceof Job) {\n // Form 1\n requestPayload.jobId = firstArg instanceof Job ? firstArg.id : firstArg;\n } else if (typeof firstArg === 'object') {\n // Form 2\n if (\n (typeof firstArg.startTime !== 'undefined' &&\n !(firstArg.startTime instanceof Date)) ||\n (typeof firstArg.endTime !== 'undefined' &&\n !(firstArg.endTime instanceof Date))\n ) {\n throw new TypeError(\n 'startTime and or endTime were not specified as instances of Date',\n );\n }\n\n requestPayload.startDeployTime = firstArg.startTime;\n requestPayload.endDeployTime = firstArg.endTime;\n } else {\n throw new TypeError(`Unknown first argument type ${typeof firstArg}`);\n }\n\n if (!this.deployConnection)\n this.openDeployConn();\n\n const {\n success,\n payload: responsePayload,\n } = await this.deployConnection.send(\n 'listJobs',\n requestPayload,\n ownerKeystore,\n );\n\n if (!success) {\n throw new DCPError('Request for job status failed', responsePayload);\n }\n\n if (typeof firstArg === 'string' || firstArg instanceof Job) {\n return responsePayload[0];\n }\n\n return responsePayload;\n }\n\n /**\n * Returns information for the job with the provided ID. It will use\n * `wallet.getId()` to retrieve the auth keystore.\n *\n * @param {string} jobAddress\n * @returns {Promise<object>} status object describing a given job\n * @access public\n */\n async getJobInfo(jobAddress) {\n if (!this.phemeConnection)\n this.openPhemeConn();\n\n const ks = await wallet.getId();\n const { payload: jobResponsePayload } = await this.phemeConnection.send('fetchJobReport', \n {\n jobOwner: ks.address,\n job: jobAddress\n });\n\n const { payload: sliceResponsePayload } = await this.phemeConnection.send(\n 'fetchSliceReport', \n {\n jobOwner: ks.address,\n job: jobAddress\n },\n );\n\n const jobReport = {};\n Object.assign(jobReport, jobResponsePayload, sliceResponsePayload);\n\n return jobReport;\n }\n\n /**\n * Returns information for the slices of the job with the provided ID. It will\n * use `wallet.getId()` to retrieve the auth keystore.\n *\n * @param {string} jobAddress\n * @returns {Promise<object>} A slice status object for the given job.\n * @access public\n */\n async getSliceInfo(jobAddress) {\n if (!this.phemeConnection)\n this.openPhemeConn();\n\n const identityKeystore = await wallet.getId();\n const {\n success,\n payload: responsePayload,\n } = await this.phemeConnection.send(\n 'fetchSliceReport',\n {\n job: jobAddress,\n jobOwner: identityKeystore.address,\n },\n identityKeystore,\n );\n\n if (!success) {\n throw new DCPError('Request for slice info failed', responsePayload);\n }\n\n return responsePayload;\n }\n}\n\nexports.RemoteDataSet = RemoteDataSet;\nexports.RemoteDataPattern = RemoteDataPattern;\nexports.MARKET_VALUE = MARKET_VALUE;\nexports.compute = new Compute(); /* :( */\nexports.compute.getPublicComputeGroupId = () => __webpack_require__(/*! dcp/common/scheduler-constants */ \"./src/common/scheduler-constants.js\").computeGroups.public.id;\nexports.compute.getPublicComputeGroupDescriptor = () => __webpack_require__(/*! dcp/common/scheduler-constants */ \"./src/common/scheduler-constants.js\").computeGroups.public;\nexports.compute.ResultHandle = ResultHandle; // export as compute.ResultHandle for instanceof checking\nexports.version = {\n api: '1.5.2',\n provides: '1.0.0' /* dcpConfig.scheduler.compatibility.operations.compute */\n};\n\n\n//# sourceURL=webpack:///./src/dcp-client/compute.js?");
5091
5080
 
5092
5081
  /***/ }),
5093
5082
 
@@ -5098,7 +5087,7 @@ eval("/**\n * @file Module that implements Compute API\n * @module dcp/comput
5098
5087
  /*! no static exports found */
5099
5088
  /***/ (function(module, exports, __webpack_require__) {
5100
5089
 
5101
- eval("/* WEBPACK VAR INJECTION */(function(module) {/**\n * @file dcp-client-bundle-src.js\n * Top-level file which gets webpacked into the bundle consumed by dcp-client 2.5\n * @author Wes Garland, wes@kingsds.network\n * @date July 2019\n */\n\n{\n let thisScript = typeof document !== 'undefined' ? (typeof document.currentScript !== 'undefined' && document.currentScript) || document.getElementById('_dcp_client_bundle') : {}\n let realModuleDeclare\n\n if ( false || typeof module.declare === 'undefined') {\n realModuleDeclare = ( true) ? module.declare : undefined\n if (false) {}\n module.declare = function moduleUnWrapper (deps, factory) {\n factory(null, module.exports, module)\n return module.exports\n }\n }\n\n let _debugging = () => false\n dcpConfig.future = __webpack_require__(/*! ../common/config-future.js */ \"./src/common/config-future.js\").futureFactory(_debugging, dcpConfig);\n\n /* These modules are official API and must be part of DCP Client */\n let officialApi = {\n 'protocol': __webpack_require__(/*! ../protocol-v4 */ \"./src/protocol-v4/index.js\"),\n 'compute': __webpack_require__(/*! ./compute */ \"./src/dcp-client/compute.js\").compute,\n 'worker': __webpack_require__(/*! ./worker */ \"./src/dcp-client/worker/index.js\"),\n 'wallet': __webpack_require__(/*! ./wallet */ \"./src/dcp-client/wallet/index.js\"),\n };\n\n /* Allow client programs to use modules which happen to be in the bundle anyhow */\n let conveniencePeers = {\n 'ethereumjs-tx': __webpack_require__(/*! ./wallet/keystore */ \"./src/dcp-client/wallet/keystore.js\")._internalEth.tx,\n 'ethereumjs-wallet': __webpack_require__(/*! ./wallet/keystore */ \"./src/dcp-client/wallet/keystore.js\")._internalEth.wallet,\n 'ethereumjs-util': __webpack_require__(/*! ./wallet/keystore */ \"./src/dcp-client/wallet/keystore.js\")._internalEth.util,\n 'socket.io-client': __webpack_require__(/*! socket.io-client */ \"./node_modules/socket.io-client/build/index.js\"),\n 'bignumber.js': __webpack_require__(/*! bignumber.js */ \"./node_modules/bignumber.js/bignumber.js\"),\n 'semver': __webpack_require__(/*! semver */ \"./node_modules/semver/semver.js\"),\n };\n\n /* Some of these modules are API-track. Some of them need to be published to be\n * available for top-level resolution by DCP internals. Those (mostly) should have\n * been written using relative module paths.....\n */\n let modules = Object.assign({\n 'protocol-v3': __webpack_require__(/*! ../protocol-v3 */ \"./src/protocol-v3/index.js\").protocol,\n 'dcp-build': {\"version\":\"dd98e423ca01eaeeba22e0bd0d948e358ffc7b43\",\"branch\":\"release\",\"dcpClient\":{\"version\":\"4.1.11\",\"from\":\"git+ssh://git@gitlab.com/Distributed-Compute-Protocol/dcp-client.git#release\",\"resolved\":\"git+ssh://git@gitlab.com/Distributed-Compute-Protocol/dcp-client.git#9275023e3d51fbc2437111118c8d3bbb73bfebc1\"},\"built\":\"Mon Sep 20 2021 11:03:08 GMT-0400 (Eastern Daylight Time)\",\"config\":{\"generated\":\"Mon 20 Sep 2021 11:03:07 AM EDT by erose on lorge\",\"build\":\"debug\"},\"webpack\":\"4.46.0\",\"node\":\"v12.22.6\"},\n 'dcp-xhr': __webpack_require__(/*! ../common/dcp-xhr */ \"./src/common/dcp-xhr.js\"),\n 'dcp-env': __webpack_require__(/*! ../common/dcp-env */ \"./src/common/dcp-env.js\"),\n 'dcp-url': __webpack_require__(/*! ../common/dcp-url */ \"./src/common/dcp-url.js\"),\n 'cli': __webpack_require__(/*! ../common/cli */ \"./src/common/cli.js\"),\n 'dcp-timers': __webpack_require__(/*! ../common/dcp-timers */ \"./src/common/dcp-timers.js\"),\n 'dcp-dot-dir': __webpack_require__(/*! ../common/dcp-dot-dir */ \"./src/common/dcp-dot-dir.js\"),\n 'dcp-assert': __webpack_require__(/*! ../common/dcp-assert */ \"./src/common/dcp-assert.js\"),\n 'dcp-events': __webpack_require__(/*! ../common/dcp-events */ \"./src/common/dcp-events/index.js\"),\n 'utils': __webpack_require__(/*! ../utils */ \"./src/utils/index.js\"),\n 'debugging': __webpack_require__(/*! ../debugging */ \"./src/debugging.js\"),\n 'publish': __webpack_require__(/*! ../common/dcp-publish */ \"./src/common/dcp-publish.js\"),\n 'compute-groups': {\n ...__webpack_require__(/*! ./compute-groups */ \"./src/dcp-client/compute-groups/index.js\"),\n publicGroupOpaqueId: __webpack_require__(/*! ../common/scheduler-constants */ \"./src/common/scheduler-constants.js\").computeGroups.public.opaqueId,\n },\n 'bank-util': __webpack_require__(/*! ./bank-util */ \"./src/dcp-client/bank-util.js\"),\n 'protocol-v4': __webpack_require__(/*! ../protocol-v4 */ \"./src/protocol-v4/index.js\"), /* deprecated */\n 'client-modal': __webpack_require__(/*! ./client-modal */ \"./src/dcp-client/client-modal/index.js\"),\n 'legacy-modal': __webpack_require__(/*! ../protocol-v3/modal */ \"./src/protocol-v3/modal.js\").Modal,\n 'eth': __webpack_require__(/*! ./wallet/eth */ \"./src/dcp-client/wallet/eth.js\"),\n 'serialize': __webpack_require__(/*! ../utils/serialize */ \"./src/utils/serialize.js\"),\n 'job': __webpack_require__(/*! ./job */ \"./src/dcp-client/job/index.js\"),\n 'range-object': __webpack_require__(/*! ./range-object */ \"./src/dcp-client/range-object.js\"),\n 'stats-ranges': __webpack_require__(/*! ./stats-ranges */ \"./src/dcp-client/stats-ranges.js\"),\n 'standard-objects': {}\n }, conveniencePeers, officialApi);\n\n /* Export the JS Standard Classes (etc) from the global object of the bundle evaluation context,\n * in case we have code somewhere that needs to use these for instanceof checks.\n */\n ;[ Object, Function, Boolean, Symbol,\n Error, EvalError, RangeError, ReferenceError, SyntaxError, TypeError, URIError,\n Number, Math, Date,\n String, RegExp,\n Array, Int8Array, Uint8Array, Uint8ClampedArray, Int16Array, Uint16Array, Int32Array, Uint32Array, Float32Array, Float64Array,\n Map, Set, WeakMap, WeakSet,\n ArrayBuffer, DataView, JSON,\n Promise, \n Reflect, Proxy, Intl, WebAssembly, __webpack_require__\n ].forEach(function (obj) {\n if (obj.name && (typeof obj === 'function' || typeof obj === 'object'))\n modules['standard-objects'][obj.name] = obj\n })\n\n if (typeof BigInt !== 'undefined')\n modules['standard-objects']['BigInt'] === BigInt;\n if (typeof BigInt64Array !== 'undefined')\n modules['standard-objects']['BigInt64Array'] === BigInt64Array;\n if (typeof BigInt64Array !== 'undefined')\n modules['standard-objects']['BigUint64Array'] === BigUint64Array;\n\n module.declare([], function(require, exports, module) {\n Object.assign(exports, modules)\n exports['dcp-config'] = dcpConfig\n })\n if (realModuleDeclare)\n module.declare = realModuleDeclare\n\n bundleExports = thisScript.exports = exports; /* must be last expression evaluated! */\n}\n\n/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(/*! ./../../node_modules/webpack/buildin/module.js */ \"./node_modules/webpack/buildin/module.js\")(module)))\n\n//# sourceURL=webpack:///./src/dcp-client/index.js?");
5090
+ eval("/* WEBPACK VAR INJECTION */(function(module) {/**\n * @file dcp-client-bundle-src.js\n * Top-level file which gets webpacked into the bundle consumed by dcp-client 2.5\n * @author Wes Garland, wes@kingsds.network\n * @date July 2019\n */\n\n{\n let thisScript = typeof document !== 'undefined' ? (typeof document.currentScript !== 'undefined' && document.currentScript) || document.getElementById('_dcp_client_bundle') : {}\n let realModuleDeclare\n\n if ( false || typeof module.declare === 'undefined') {\n realModuleDeclare = ( true) ? module.declare : undefined\n if (false) {}\n module.declare = function moduleUnWrapper (deps, factory) {\n factory(null, module.exports, module)\n return module.exports\n }\n }\n\n let _debugging = () => false\n dcpConfig.future = __webpack_require__(/*! ../common/config-future.js */ \"./src/common/config-future.js\").futureFactory(_debugging, dcpConfig);\n\n /* These modules are official API and must be part of DCP Client */\n let officialApi = {\n 'protocol': __webpack_require__(/*! ../protocol-v4 */ \"./src/protocol-v4/index.js\"),\n 'compute': __webpack_require__(/*! ./compute */ \"./src/dcp-client/compute.js\").compute,\n 'worker': __webpack_require__(/*! ./worker */ \"./src/dcp-client/worker/index.js\"),\n 'wallet': __webpack_require__(/*! ./wallet */ \"./src/dcp-client/wallet/index.js\"),\n };\n\n /* Allow client programs to use modules which happen to be in the bundle anyhow */\n let conveniencePeers = {\n 'ethereumjs-tx': __webpack_require__(/*! ./wallet/keystore */ \"./src/dcp-client/wallet/keystore.js\")._internalEth.tx,\n 'ethereumjs-wallet': __webpack_require__(/*! ./wallet/keystore */ \"./src/dcp-client/wallet/keystore.js\")._internalEth.wallet,\n 'ethereumjs-util': __webpack_require__(/*! ./wallet/keystore */ \"./src/dcp-client/wallet/keystore.js\")._internalEth.util,\n 'socket.io-client': __webpack_require__(/*! socket.io-client */ \"./node_modules/socket.io-client/build/index.js\"),\n 'bignumber.js': __webpack_require__(/*! bignumber.js */ \"./node_modules/bignumber.js/bignumber.js\"),\n 'semver': __webpack_require__(/*! semver */ \"./node_modules/semver/semver.js\"),\n };\n\n /* Some of these modules are API-track. Some of them need to be published to be\n * available for top-level resolution by DCP internals. Those (mostly) should have\n * been written using relative module paths.....\n */\n let modules = Object.assign({\n 'protocol-v3': __webpack_require__(/*! ../protocol-v3 */ \"./src/protocol-v3/index.js\").protocol,\n 'dcp-build': {\"version\":\"e4988ba0b1c993e062fc7c536188bda3ea4de883\",\"branch\":\"release\",\"dcpClient\":{\"version\":\"4.1.12\",\"from\":\"git+ssh://git@gitlab.com/Distributed-Compute-Protocol/dcp-client.git#release\",\"resolved\":\"git+ssh://git@gitlab.com/Distributed-Compute-Protocol/dcp-client.git#169b90c5603b765cbd334308bb8ac57eddb28378\"},\"built\":\"Fri Oct 22 2021 16:01:33 GMT-0400 (Eastern Daylight Time)\",\"config\":{\"generated\":\"Fri 22 Oct 2021 04:01:30 PM EDT by erose on dione\",\"build\":\"debug\"},\"webpack\":\"4.46.0\",\"node\":\"v12.22.6\"},\n 'dcp-xhr': __webpack_require__(/*! ../common/dcp-xhr */ \"./src/common/dcp-xhr.js\"),\n 'dcp-env': __webpack_require__(/*! ../common/dcp-env */ \"./src/common/dcp-env.js\"),\n 'dcp-url': __webpack_require__(/*! ../common/dcp-url */ \"./src/common/dcp-url.js\"),\n 'cli': __webpack_require__(/*! ../common/cli */ \"./src/common/cli.js\"),\n 'dcp-timers': __webpack_require__(/*! ../common/dcp-timers */ \"./src/common/dcp-timers.js\"),\n 'dcp-dot-dir': __webpack_require__(/*! ../common/dcp-dot-dir */ \"./src/common/dcp-dot-dir.js\"),\n 'dcp-assert': __webpack_require__(/*! ../common/dcp-assert */ \"./src/common/dcp-assert.js\"),\n 'dcp-events': __webpack_require__(/*! ../common/dcp-events */ \"./src/common/dcp-events/index.js\"),\n 'utils': __webpack_require__(/*! ../utils */ \"./src/utils/index.js\"),\n 'debugging': __webpack_require__(/*! ../debugging */ \"./src/debugging.js\"),\n 'publish': __webpack_require__(/*! ../common/dcp-publish */ \"./src/common/dcp-publish.js\"),\n 'compute-groups': {\n ...__webpack_require__(/*! ./compute-groups */ \"./src/dcp-client/compute-groups/index.js\"),\n publicGroupOpaqueId: __webpack_require__(/*! ../common/scheduler-constants */ \"./src/common/scheduler-constants.js\").computeGroups.public.opaqueId,\n },\n 'bank-util': __webpack_require__(/*! ./bank-util */ \"./src/dcp-client/bank-util.js\"),\n 'protocol-v4': __webpack_require__(/*! ../protocol-v4 */ \"./src/protocol-v4/index.js\"), /* deprecated */\n 'client-modal': __webpack_require__(/*! ./client-modal */ \"./src/dcp-client/client-modal/index.js\"),\n 'legacy-modal': __webpack_require__(/*! ../protocol-v3/modal */ \"./src/protocol-v3/modal.js\").Modal,\n 'eth': __webpack_require__(/*! ./wallet/eth */ \"./src/dcp-client/wallet/eth.js\"),\n 'serialize': __webpack_require__(/*! ../utils/serialize */ \"./src/utils/serialize.js\"),\n 'job': __webpack_require__(/*! ./job */ \"./src/dcp-client/job/index.js\"),\n 'range-object': __webpack_require__(/*! ./range-object */ \"./src/dcp-client/range-object.js\"),\n 'stats-ranges': __webpack_require__(/*! ./stats-ranges */ \"./src/dcp-client/stats-ranges.js\"),\n 'standard-objects': {}\n }, conveniencePeers, officialApi);\n\n /* Export the JS Standard Classes (etc) from the global object of the bundle evaluation context,\n * in case we have code somewhere that needs to use these for instanceof checks.\n */\n ;[ Object, Function, Boolean, Symbol,\n Error, EvalError, RangeError, ReferenceError, SyntaxError, TypeError, URIError,\n Number, Math, Date,\n String, RegExp,\n Array, Int8Array, Uint8Array, Uint8ClampedArray, Int16Array, Uint16Array, Int32Array, Uint32Array, Float32Array, Float64Array,\n Map, Set, WeakMap, WeakSet,\n ArrayBuffer, DataView, JSON,\n Promise, \n Reflect, Proxy, Intl, WebAssembly, __webpack_require__\n ].forEach(function (obj) {\n if (obj.name && (typeof obj === 'function' || typeof obj === 'object'))\n modules['standard-objects'][obj.name] = obj\n })\n\n if (typeof BigInt !== 'undefined')\n modules['standard-objects']['BigInt'] === BigInt;\n if (typeof BigInt64Array !== 'undefined')\n modules['standard-objects']['BigInt64Array'] === BigInt64Array;\n if (typeof BigInt64Array !== 'undefined')\n modules['standard-objects']['BigUint64Array'] === BigUint64Array;\n\n module.declare([], function(require, exports, module) {\n Object.assign(exports, modules)\n exports['dcp-config'] = dcpConfig\n })\n if (realModuleDeclare)\n module.declare = realModuleDeclare\n\n bundleExports = thisScript.exports = exports; /* must be last expression evaluated! */\n}\n\n/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(/*! ./../../node_modules/webpack/buildin/module.js */ \"./node_modules/webpack/buildin/module.js\")(module)))\n\n//# sourceURL=webpack:///./src/dcp-client/index.js?");
5102
5091
 
5103
5092
  /***/ }),
5104
5093
 
@@ -5110,7 +5099,7 @@ eval("/* WEBPACK VAR INJECTION */(function(module) {/**\n * @file dcp-cli
5110
5099
  /***/ (function(module, exports, __webpack_require__) {
5111
5100
 
5112
5101
  "use strict";
5113
- eval("/**\n * @file job/index.js\n * @author Eddie Roosenmaallen, eddie@kingsds.network\n * Matthew Palma, mpalma@kingsds.network\n * @date November 2018\n *\n * This module implements the Compute API's Job Handle\n *\n */\n\n/** @typedef {import('dcp/dcp-client/wallet/keystore').Keystore} Keystore */\n\n\nconst BigNumber = __webpack_require__(/*! bignumber.js */ \"./node_modules/bignumber.js/bignumber.js\");\nconst { v4: uuidv4 } = __webpack_require__(/*! uuid */ \"./node_modules/uuid/dist/esm-browser/index.js\");\nconst { EventEmitter, PropagatingEventEmitter } = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\");\nconst { RangeObject, MultiRangeObject, DistributionRange, SuperRangeObject } = __webpack_require__(/*! dcp/dcp-client/range-object */ \"./src/dcp-client/range-object.js\");\nconst { fetchURI, encodeDataURI, dumpObject } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\nconst serialize = __webpack_require__(/*! dcp/utils/serialize */ \"./src/utils/serialize.js\");\nconst wallet = __webpack_require__(/*! dcp/dcp-client/wallet */ \"./src/dcp-client/wallet/index.js\");\nconst protocolV4 = __webpack_require__(/*! dcp/protocol-v4 */ \"./src/protocol-v4/index.js\");\nconst { EventSubscriber } = __webpack_require__(/*! dcp/events/event-subscriber */ \"./src/events/event-subscriber.js\");\nconst { DcpURL } = __webpack_require__(/*! dcp/common/dcp-url */ \"./src/common/dcp-url.js\");\nconst ClientModal = __webpack_require__(/*! dcp/dcp-client/client-modal */ \"./src/dcp-client/client-modal/index.js\");\nconst { Worker } = __webpack_require__(/*! dcp/dcp-client/worker */ \"./src/dcp-client/worker/index.js\");\nconst RemoteDataSet = __webpack_require__(/*! dcp/dcp-client/remote-data-set */ \"./src/dcp-client/remote-data-set.js\");\nconst { ResultHandle } = __webpack_require__(/*! ./result-handle */ \"./src/dcp-client/job/result-handle.js\");\nconst { SlicePaymentOffer } = __webpack_require__(/*! ./slice-payment-offer */ \"./src/dcp-client/job/slice-payment-offer.js\");\nconst DCP_ENV = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\nconst dcpPublish = __webpack_require__(/*! dcp/common/dcp-publish */ \"./src/common/dcp-publish.js\");\nconst computeGroups = __webpack_require__(/*! dcp/dcp-client/compute-groups */ \"./src/dcp-client/compute-groups/index.js\");\nconst schedulerConstants = __webpack_require__(/*! dcp/common/scheduler-constants */ \"./src/common/scheduler-constants.js\");\nconst bankUtil = __webpack_require__(/*! dcp/dcp-client/bank-util */ \"./src/dcp-client/bank-util.js\");\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\nconst debugging = __webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope('dcp-client');\nconst kvin = __webpack_require__(/*! kvin */ \"./node_modules/kvin/kvin.js\");\n\nconst log = (...args) => {\n if (debugging('job')) {\n console.debug('dcp-client:job', ...args);\n }\n};\n\nconst ON_BROWSER = DCP_ENV.isBrowserPlatform;\nconst sideloaderModuleIdentifier = 'sideloader-v1';\n\n// Symbols used to hide private members and functions on the Job instance\nconst debugBuild = (__webpack_require__(/*! dcp/common/dcp-build */ \"./src/common/dcp-build.js\").build === 'debug');\nconst INTERNAL_SYMBOL = debugBuild ? '_' : Symbol('Job Internals');\nconst SNAPSHOT = debugBuild ? '_snapshot' : Symbol('Job.snapshot');\nconst DEPLOY_JOB = debugBuild ? '_deploy' : Symbol('Job.deploy');\n\nconst ADD_LISTENERS = Symbol('Job.addListeners');\nconst LISTEN_TO_EVENTS = Symbol('Job.listenToEvents');\nconst LISTEN_TO_WORK_EVENTS = Symbol('Job.listenToWorkEvents');\nconst ON_RESULT = Symbol('Job.onResult');\nconst ON_STATUS = Symbol('Job.onStatus');\n\nexports.JOB_INTERNAL_SYMBOL = INTERNAL_SYMBOL; /* allow friends to access our guts, eg. job/result-handle */\n\nconst DEFAULT_REQUIREMENTS = {\n engine: {\n es7: null,\n spidermonkey: null\n },\n environment: {\n webgpu: null,\n offscreenCanvas: null,\n fdlibm: null\n },\n browser: {\n chrome: null\n },\n details: {\n offscreenCanvas: {\n bigTexture4096: null,\n bigTexture8192: null,\n bigTexture16384: null,\n bigTexture32768: null,\n }\n },\n discrete: null,\n};\nconst RELIABLE_EVENTS = [\n 'complete', 'cancel', 'ENOFUNDS', 'ETOOMANYERRORS', 'ESLICETOOSLOW',\n 'ENOPROGRESS', 'ETOOMANYTASKS', 'EWORKTOOBIG', 'ETOOBIG',\n];\nconst OPTIONAL_EVENTS = [\n 'console', 'error', 'noProgress', 'noProgressData',\n];\nconst ZERO_COST_HOLD_ADDRESS = '0x' + '0'.repeat(130);\n\n/** @typedef {import('../range-object').RangeLike} RangeLike */\n\n/**\n * Ensure input data is an appropriate format\n * @param {RangeObject | DistributionRange | RemoteDataSet | Array | Iterable}\n * inputData - A URI-shaped string, a [Multi]RangeObject-constructing value, or\n * an array of slice data\n * @return {RangeObject | RangeLike | DistributionRange | RemoteDataSet | Array}\n * The coerced input in an appropriate format ([Multi]RangeObject,\n * DistributionRange, RemoteDataSet, or array)\n */\nconst wrangleData = (inputData) => {\n if (typeof inputData === 'object' && !!inputData.ranges) { return new MultiRangeObject(inputData) }\n\n if (RangeObject.isRangelike(inputData)) { return inputData }\n if (RangeObject.isRangeObject(inputData)) { return inputData }\n if (DistributionRange.isDistribution(inputData)) { return inputData }\n if (RangeObject.isProtoRangelike(inputData)) { return new RangeObject(inputData) }\n if (DistributionRange.isProtoDistribution(inputData)) { return new DistributionRange(inputData) }\n if (RemoteDataSet.isRemoteDataSet(inputData)) {\n return new RemoteDataSet(...inputData);\n }\n\n return Array.isArray(inputData) ? inputData : [inputData];\n};\n\n// Used to validate the requirements object,\n// applies the default requirements schema\nconst applyObjectSchema = (obj, schema, errContext='', scope='') => {\n let checkedObjs = [];\n\n for (let p in schema) {\n let fullPropScope = scope.concat(p);\n if (!(p in obj)) {\n if (typeof schema[p] === 'object' && schema[p] !== null) {\n obj[p] = {};\n checkedObjs.push(fullPropScope);\n applyObjectSchema(obj[p], schema[p], errContext, fullPropScope.concat('.'));\n } else obj[p] = schema[p];\n } else if (typeof schema[p] === 'object' && schema[p] !== null && !checkedObjs.includes(fullPropScope)) {\n if (typeof obj[p] !== 'object') throw new Error(`${errContext}: Schema mismatch, property '${fullPropScope}' should be an object.`);\n else {\n checkedObjs.push(fullPropScope);\n applyObjectSchema(obj[p], schema[p], errContext, fullPropScope.concat('.'));\n }\n } else if ((typeof schema[p] !== 'object' || schema[p] === null)\n && typeof obj[p] !== 'boolean' && obj[p] !== null) {\n throw new Error(`${errContext}: Schema mismatch, object property '${fullPropScope}' should be a boolean.`);\n }\n }\n\n for (let p in obj) {\n let fullPropScope = scope.concat(p);\n if (!(p in schema)) throw new Error(`${errContext}: Schema mismatch, object has extra key '${fullPropScope}'.`);\n else if (typeof obj[p] === 'object' && obj[p] !== null && !checkedObjs.includes(fullPropScope)) {\n checkedObjs.push(fullPropScope);\n applyObjectSchema(obj[p], schema[p], errContext, fullPropScope.concat('.'));\n }\n }\n}\n\n/**\n * @classdesc The Compute API's Job Handle (see {@link https://docs.dcp.dev/specs/compute-api.html#job-handles|Compute API spec})\n * Job handles are objects which correspond to jobs. \n * They are created by some exports of the compute module, such as {@link module:dcp/compute.do|compute.do} and {@link module:dcp/compute.for|compute.for}.\n * @extends module:dcp/dcp-events.PropagatingEventEmitter\n * @hideconstructor\n * @access public\n */\nclass Job extends PropagatingEventEmitter {\n /**\n * This event is emitted when the job is accepted by the scheduler on deploy.\n * \n * @event Job#accepted\n * @access public\n * @type {object}\n * @property {object} job Original object that was delivered to the scheduler for deployment\n *//**\n * Fired when the job is cancelled.\n * \n * @event Job#cancel\n * @access public\n *//**\n * Fired when a result is returned.\n * \n * @event Job#result\n * @access public\n * @type {object}\n * @property {string} jobAddress Address of the job\n * @property {string} task ID of the task (slice) the result came from\n * @property {number} sort The index of the slice\n * @property {object} result\n * @property {string} result.request\n * @property {*} result.result The value returned from the work function\n *//**\n * Fired when the result handle is modified, either when a new `result` event is fired or when the results are populated with `results.fetch()`\n * \n * @event Job#resultsUpdated\n * @access public\n *//**\n * Fired when the job has been completed.\n * \n * @event Job#complete\n * @access public\n * @type {ResultHandle}\n *//**\n * Fired when the job's status changes.\n * \n * @event Job#status\n * @access public\n * @type {object}\n * @property {string} jobAddress Address of the job\n * @property {number} total Total number of slices in the job\n * @property {number} distributed Number of slices that have been distributed\n * @property {number} computed Number of slices that have completed execution (returned a result)\n * @property {string} runStatus Current runStatus of the job\n *//**\n * Fired when a slice throws an error.\n * \n * @event Job#error\n * @access public\n * @type {object}\n * @property {string} jobAddress Address of the job\n * @property {number} sliceIndex Index of the slice that threw the error\n * @property {string} message The error message\n * @property {string} stack The error stacktrace\n * @property {string} name The error type name\n *//**\n * Fired when a slice uses one of the console log functions.\n * \n * @event Job#console\n * @access public\n * @type {object}\n * @property {string} jobAddress Address of the job\n * @property {number} sliceIndex The index of the slice that produced this event\n * @property {string} level The log level, one of `debug`, `info`, `log`, `warn`, or `error`\n * @property {string} message The console log message\n *//**\n * Fired when a slice is stopped for not calling progress. Contains information about how long the slice ran for, and about the last reported progress calls.\n * \n * @event Job#noProgress\n * @access public\n * @type {object}\n * @property {string} jobAddress Address of the job\n * @property {number} sliceIndex The index of the slice that failed due to no progress\n * @property {number} timestamp How long the slice ran before failing\n * @property {object} progressReports\n * @property {object} progressReports.last The last progress report received from the worker\n * @property {number} progressReports.last.timestamp Time since the start of the slice\n * @property {number} progressReports.last.progress Progress value reported\n * @property {*} progressReports.last.value The last value that was passed to the progress function\n * @property {number} progressReports.last.throttledReports Number of calls to progress that were throttled since the last report\n * @property {object} progressReports.lastUpdate The last determinate (update to the progress param) progress report received from the worker\n * @property {number} progressReports.lastUpdate.timestamp\n * @property {number} progressReports.lastUpdate.progress\n * @property {*} progressReports.lastUpdate.value\n * @property {number} progressReports.lastUpdate.throttledReports\n *//**\n * Identical to `noProgress`, except that it also contains the data that the slice was executed with.\n * \n * @event Job#noProgressData\n * @access public\n * @type {object}\n * @property {*} data The data that the slice was executed with\n *//**\n * Fired when the job is paused due to running out of funds. The job can be resumed by escrowing more funds then resuming the job.\n * \n * Event payload is the estimated funds required to complete the job\n * \n * @event Job#ENOFUNDS\n * @access public\n * @type {BigNumber}\n *//**\n * Fired when the job is cancelled due to the work function not calling the `progress` method frequently enough.\n * \n * @event Job#ENOPROGRESS\n * @access public\n *//**\n * The job was cancelled because scheduler has determined that individual tasks in this job exceed the maximum allowable execution time.\n * \n * @event Job#ESLICETOOSLOW\n * @access public\n *//**\n * Fired when the job is cancelled because too many work functions are terminating with uncaught exceptions.\n * \n * @event Job#ETOOMANYERRORS\n * @access public\n */\n\n /**\n * @form1 new Job(job_shaped_object)\n * @form2 new Job('application_worker_address'[, data[, arguments]])\n * @form3b new Job('worker source'[, data[, arguments]])\n * @form3b new Job(worker_function[, data[, arguments]])\n */\n\n constructor() {\n super('Job');\n\n this.readyStateChange = (readyState) => {\n this.readyState = readyState;\n this.emit('readyStateChange', this.readyState);\n };\n this.readyStateChange('new');\n \n /*\n * Private members\n */\n this[INTERNAL_SYMBOL] = {\n events: new EventEmitter('Job Internal'),\n connected: false, // set to true after first call to exec\n /**\n * This object holds details for generating DCPv4 messages about this job.\n * It is updated everytime we call SNAPSHOT.\n */\n payloadDetails: {\n localExec: false,\n },\n\n /**\n * The slicePaymentOffer default value is set to compute.marketValue, in .exec() \n */\n slicePaymentOffer: null,\n paymentAccountKeystore: null,\n\n /**\n * These are private but getters are provided so they can be modified but\n * not replaced.\n */\n /**\n * List of module prefixes using in CommonJS module resolution.\n * @type {string[]}\n */\n requirePath: [],\n\n /**\n * List of modules the job needs.\n * @type {string[]}\n */\n\n dependencies: [],\n\n // This array contains the names of worker events that\n // had listeners registered before exec is called, once\n // the job has been deployed then the proper event handlers\n // will be generated from this list\n subscribedEvents: new Set(),\n subscribedWorkerEvents: new Set(),\n\n results: [],\n resultsAvailable: [],\n resultStorageType: 'values',\n resultStorageDetails: undefined,\n resultStorageParams: undefined, //Holds the POST params and URL for off-prem storage\n\n // Tracks job progress\n status: {\n runStatus: null,\n total: null,\n distributed: null,\n computed: null,\n },\n\n // Cancel is special. We need to fire an `alert` when the job is canceled. \n // If they are listening for the (reliable) event then they need to be able to\n // prevent it. If not, then it'll be handled by the `exec` rejection via the 'stopped'\n // event. The result is that we want only one of two ways the `alert` can be fired\n // to be active based on whether or not the user is listening for cancel. \n // Once DCP-1150 lands, we won't need to listen on stopped since more failures will fire a cancel event.\n listeningForCancel: false,\n // TODO - cancel events should have more info in them. DCP-1150\n cancelAlert: () => ClientModal.alert(\"More details in console...\", {title: 'Job Canceled'}),\n\n listeningForError: false,\n errorAlert: (err) => ClientModal.alert(err, {title: 'Unexpected Error'}),\n\n listeningForNoFunds: false,\n noFundsAlert: (event) => {\n let msg = `Job \"${event.name}\" is paused due to insufficient funds. ${event.fundsRequired} DCC is required to compute remaining ${event.remainingSlices} slices.\\njobId: ${event.job}\\nbankAccount: ${event.bankAccount}`; \n ClientModal.alert(msg, { title: 'Job paused' })\n },\n };\n\n /*\n * Public members\n */\n // Deep copy the default requirements. I know, I hate it too\n /**\n * An object describing the requirements that workers must have to be eligible for this job. See\n * {@link https://docs.dcp.dev/specs/compute-api.html#requirements-objects|Requirements Objects}.\n *\n * @type {object}\n * @access public\n */\n this.requirements = JSON.parse(JSON.stringify(DEFAULT_REQUIREMENTS));\n this.schedulerURL = undefined;\n this.bankURL = undefined;\n this.deployURL = '';\n /**\n * @see {@link https://kingsds.atlassian.net/browse/DCP-1475?atlOrigin=eyJpIjoiNzg3NmEzOWE0OWI4NGZkNmI5NjU0MWNmZGY2OTYzZDUiLCJwIjoiaiJ9|Jira Issue}\n */\n let uuid = uuidv4();\n\n /**\n * An object describing the cost the user believes each the average slice will incur, in terms of CPU/GPU and I/O.\n * If defined, this object is used to provide initial scheduling hints and to calculate escrow amounts.\n *\n * @type {object}\n * @access public\n */\n this.initialSliceProfile = undefined;\n\n /**\n * A place to store public-facing attributes of the job. Anything stored on this object will be available inside the work \n * function (see {@link module:dcp/compute~sandboxEnv.work}). The properties documented here may be used by workers to display what jobs are currently being \n * worked on.\n * @access public\n * @property {string} name Public-facing name of this job.\n * @property {string} description Public-facing description for this job.\n * @property {string} link Public-facing link to external resource about this job.\n */\n this.public = {\n name: null,\n description: null,\n link: null,\n };\n\n this.contextId = null\n\n // The following public members are only populated after the job has been deployed\n this.address = null;\n this.receipt = null;\n this.meanSliceProfile = null;\n\n /* We avoid using job.id internally because it is easy to confuse with db::jobs.id, but is an API\n * interface that we present to end-user developers so we need to keep it.\n */\n Object.defineProperty(this, 'id', {\n get: () => this.address,\n set: (id) => this.address = id\n });\n \n // \n /**\n * An EventEmitter for custom events dispatched by the work function.\n * @type {module:dcp/dcp-events.EventEmitter}\n * @access public\n * @example\n * // in sandbox\n * work.emit('myEventName', 1, [2], \"three\");\n * // clientside\n * job.work.on('myEventName', (num, arr, string) => { });\n */\n this.work = new EventEmitter('job.work');\n\n //Initialize the eventSubscriber so each job has unique eventSubscriber\n this.eventSubscriber = new EventSubscriber()\n \n this.work.on('newListener', (evt) => {\n if (!this[INTERNAL_SYMBOL].connected && evt !== 'newListener') {\n this[INTERNAL_SYMBOL].subscribedWorkerEvents.add(evt);\n }\n });\n\n this.on('newListener', (evt) => {\n if (!this[INTERNAL_SYMBOL].connected && evt !== 'newListener') {\n this[INTERNAL_SYMBOL].subscribedEvents.add(evt);\n }\n if (evt === 'cancel') {\n this[INTERNAL_SYMBOL].listeningForCancel = true;\n }\n\n if (evt === 'error') {\n this[INTERNAL_SYMBOL].listeningForError = true;\n }\n\n if (evt === 'nofunds') {\n this[INTERNAL_SYMBOL].listeningForNoFunds = true;\n }\n });\n\n this.addDefaultListener('cancel', (msg) => {\n if (ON_BROWSER && this[INTERNAL_SYMBOL].listeningForCancel) \n this[INTERNAL_SYMBOL].cancelAlert(msg);\n });\n\n this.addDefaultListener('error', (event) => {\n if (!this[INTERNAL_SYMBOL].listeningForError) {\n if (ON_BROWSER) {\n this[INTERNAL_SYMBOL].errorAlert(event);\n } else {\n console.error(event);\n }\n }\n });\n\n this.addDefaultListener('nofunds', (event) => {\n if (!this[INTERNAL_SYMBOL].listeningForNoFunds) {\n if (ON_BROWSER) {\n this[INTERNAL_SYMBOL].noFundsAlert(event);\n } else {\n console.warn(\n `Job ${event.job} has been ${event.runStatus} with ${event.remainingSlices} slices remaining due to running out of funds (need ${event.fundsRequired} credits). Re-add the necessary funds and resume the job to continue getting results.`,\n );\n }\n }\n });\n\n // Form1: If arguments[0] is an object that looks like a job, this is a 'copy constructor'\n // where we inherit as much as possible from the original instance.\n if (typeof arguments[0] === 'object' &&\n arguments[0].type &&\n arguments[0].data &&\n arguments[0].public) {\n \n let src = arguments[0];\n\n this[INTERNAL_SYMBOL].payloadDetails = {\n ...src,\n data: wrangleData(src.data), // rehydrate ranges\n };\n\n if (src.feeStructure) {\n this.setSlicePaymentOffer(src.feeStructure);\n }\n \n if (src.address)\n this.address = src.address;\n if (src.payloadData.status)\n this[ON_STATUS](this[INTERNAL_SYMBOL].payload.status, false);\n if (src.public)\n Object.assign(this.public, src.public);\n } else {\n /* Forms 2-4 */ \n if (typeof arguments[0] === 'function')\n arguments[0] = arguments[0].toString();\n\n if (typeof arguments[0] === 'string') {\n this[INTERNAL_SYMBOL].workFunctionURI = encodeDataURI(arguments[0], 'application/javascript');\n } else if (DcpURL.isURL(arguments[0])) {\n this[INTERNAL_SYMBOL].workFunctionURI = arguments[0].href;\n } \n\n const wrangledInputData = wrangleData(arguments[1] || []);\n log('wrangledInputData:', wrangledInputData);\n Object.assign(this[INTERNAL_SYMBOL].payloadDetails, {\n request: 'main',\n data: wrangledInputData,\n arguments: arguments[2] || [],\n });\n }\n\n // This should happen last, it depends on the this.[INTERNAL_SYMBOL].payloadDetails.data array\n /**\n * A Result Handle object used to query and manipulate the output set. \n * Present once job has been deployed.\n * @type {ResultHandle}\n * @access public\n */\n this.results = new ResultHandle(this);\n\n /**\n * Read-only copy of the job's uuid (generated or rehydrated via form1 constructor)\n */\n Object.defineProperty(this, 'uuid', {\n get: () => uuid,\n configurable: false,\n enumerable: true,\n });\n \n // each entry contains the computeGroupID, joinKey, joinSecret, joinKeystore\n // Add to public compute group by default\n this.computeGroups = [ Object.assign({}, schedulerConstants.computeGroups.public) ];\n\n\n const ceci = this;\n\n // Initialize to null so these properties are recognized for the Job class\n this.bankConnection = null;\n this.deployConnection = null;\n this.openBankConn = function openBankConn()\n {\n ceci.bankConnection = new protocolV4.Connection(dcpConfig.bank.services.bankTeller);\n ceci.bankConnection.on('close', ceci.openBankConn);\n }\n\n this.openDeployConn = function openDeployConn()\n {\n ceci.deployConnection = new protocolV4.Connection(dcpConfig.scheduler.services.jobSubmit);\n ceci.deployConnection.on('close', ceci.openDeployConn);\n }\n\n this.openBankConn();\n this.openDeployConn();\n }\n\n /** \n * Cancel the job\n * @access public\n * @param {string} reason If provided, will be sent to client\n */\n async cancel (reason = undefined) {\n const response = await this.deployConnection.send('cancelJob', {\n job: this.address,\n ownerAddress: this.paymentAccountKeystore.address,\n reason,\n }, this.paymentAccountKeystore);\n\n return response.payload;\n }\n\n /** \n * Resume this job\n * @access public\n */\n async resume() {\n const response = await this.schedulerConnection.send('resumeJob', {\n job: this.address,\n owner: this.paymentAccountKeystore.address,\n }, this.paymentAccountKeystore);\n\n return response.payload;\n }\n\n /**\n * Helper function for retrieving info about the job. The job must have already been deployed.\n * An alias for {@link module:dcp/compute.getJobInfo}.\n * @access public\n */\n async getJobInfo(){\n return await __webpack_require__(/*! ../compute */ \"./src/dcp-client/compute.js\").compute.getJobInfo(this.address);\n }\n\n /**\n * Helper function for retrieving info about the job's slices. The job must have already been deployed.\n * An alias for {@link module:dcp/compute.getSliceInfo}.\n * @access public\n */\n async getSliceInfo(){\n return await __webpack_require__(/*! ../compute */ \"./src/dcp-client/compute.js\").compute.getSliceInfo(this.address);\n }\n \n async addSliceData(dataValues) {\n if (!Array.isArray(this[INTERNAL_SYMBOL].payloadDetails.data)) {\n throw new TypeError('Only data-by-value jobs may dynamically add slices');\n }\n\n const { payload } = await this.deployConnection.send('addSliceData', {\n job: this.address,\n dataValues,\n });\n\n if (payload.success && typeof payload.lastSliceNumber !== 'undefined')\n this.status.total = payload.lastSliceNumber;\n\n return payload;\n }\n\n /**\n * job.snapshot(): Private function used to populate the payloadDetails from private data,\n * inferred data, etc. Once this function has run, the payloadDetails are\n * considered authoritatively up to date until the calling function returns\n * or awaits.\n */\n [SNAPSHOT]() {\n const pd = this[INTERNAL_SYMBOL].payloadDetails;\n\n pd.type = 'ad-hoc'; /* @todo implement appliances */\n pd.uuid = this.uuid;\n pd.workFunctionURI = this[INTERNAL_SYMBOL].workFunctionURI;\n pd.dependencies = this[INTERNAL_SYMBOL].dependencies;\n pd.requirePath = this[INTERNAL_SYMBOL].requirePath;\n pd.modulePath = this[INTERNAL_SYMBOL].modulePath;\n pd.resultStorageType = this[INTERNAL_SYMBOL].resultStorageType;\n pd.resultStorageDetails = this[INTERNAL_SYMBOL].resultStorageDetails;\n pd.resultStorageParams = this[INTERNAL_SYMBOL].resultStorageParams;\n\n pd.requirements = this.requirements;\n applyObjectSchema(pd.requirements, DEFAULT_REQUIREMENTS, 'Requirements Object');\n \n // @todo: 'figure this out' - wise words from eddie /mp jan 2019\n if (!pd.options) { pd.options = {}; }\n if (!pd.public) { pd.public = {}; } \n\n for (let p of ['name', 'description', 'link']) {\n if (typeof this.public[p] === 'string') {\n pd.public[p] = this.public[p];\n }\n }\n\n // The max value that the client is willing to spend to deploy\n // (list on the scheduler, doesn't include compute payment)\n /// maxDeployPayment is the max the user is willing to pay to DCP (as a\n /// Hold), in addition to the per-slice offer and associated scrape.\n /// Currently calculated as `deployCost = costPerKB *\n /// (JSON.stringify(job).length / 1024) // 1e-9 per kb`\n // @todo: figure this out / er nov 2018\n pd.maxDeployPayment = 1;\n \n /// payloadDetails.timing can be provided as an initial estimate of slice time, to\n /// give a more useful useful calculated heap value (heap.value is more or less\n /// dcc-per-millisecond)\n pd.timing = pd.timing || 1; \n }\n\n /** Escrow additional funds for this job\n * @access public\n * @param {number|BigNumber} fundsRequired - A number or BigNumber instance representing the funds to escrow for this job\n */\n async escrow (fundsRequired) {\n if ((typeof fundsRequired !== 'number' && !BigNumber.isBigNumber(fundsRequired))\n || fundsRequired <= 0 || !Number.isFinite(fundsRequired) || Number.isNaN(fundsRequired)) {\n throw new Error(`Job.escrow: fundsRequired must be a number greater than zero. (not ${fundsRequired})`);\n }\n\n const response = await this.bankConnection.send('embiggenFeeStructure', {\n feeStructureAddress: this[INTERNAL_SYMBOL].payloadDetails.feeStructureId,\n additionalEscrow: BigNumber(fundsRequired),\n fromAddress: this.paymentAccountKeystore.address,\n }, this.paymentAccountKeystore);\n\n this.receipt = response.payload;\n\n return this.receipt;\n }\n\n async _pack () {\n var retval = __webpack_require__(/*! ./node-modules */ \"./src/dcp-client/job/node-modules.js\").createModuleBundle(this[INTERNAL_SYMBOL].dependencies);\n return retval;\n }\n\n /** \n * Collect all of the dependencies together, throw them into a BravoJS\n * module which sideloads them as a side effect of declaration, and transmit\n * them to the package manager. Then we return the package descriptor object,\n * which is guaranteed to have only one file in it.\n *\n * @returns {object} with properties name and files[0]\n */\n async _publishLocalModules() {\n const { tempFile, hash, unresolved } = await this._pack();\n\n if (!tempFile) {\n return { unresolved };\n }\n\n const sideloaderFilename = tempFile.filename;\n const pkg = {\n name: `dcp-pkg-v1-localhost-${hash.toString('hex')}`,\n version: '1.0.0',\n files: {\n [sideloaderFilename]: `${sideloaderModuleIdentifier}.js`,\n },\n }\n\n await dcpPublish.publish(pkg);\n tempFile.remove();\n\n return { pkg, unresolved };\n }\n\n /**\n * Deploys the job to the scheduler.\n * @param {number | object} [slicePaymentOffer=compute.marketValue] - Amount\n * in DCC that the user is willing to pay per slice.\n * @param {Keystore} [paymentAccountKeystore=wallet.get] - An instance of the\n * Wallet API Keystore that's used as the payment account when executing the\n * job.\n * @param {object} [initialSliceProfile] - An object describing the cost the\n * user believes the average slice will incur.\n * @access public\n * @emits Job#accepted\n */\n async exec(slicePaymentOffer = __webpack_require__(/*! ../compute */ \"./src/dcp-client/compute.js\").compute.marketValue, paymentAccountKeystore, initialSliceProfile) {\n if (this[INTERNAL_SYMBOL].connected) {\n throw new Error('Exec called twice on the same job handle.');\n }\n\n /* eagerly connect to depedent services for better performance */\n this.eventSubscriber.eventRouterConnection.keepalive();\n this.deployConnection.keepalive();\n\n this.readyState = 'exec'\n this.emit('readystatechange', this.readyState)\n\n if (typeof slicePaymentOffer !== 'undefined') this.setSlicePaymentOffer(slicePaymentOffer);\n if (typeof initialSliceProfile !== 'undefined') this.initialSliceProfile = initialSliceProfile;\n if (typeof paymentAccountKeystore !== 'undefined') {\n /** XXX @todo deprecate use of ethereum wallet objects */\n if (typeof paymentAccountKeystore === 'object' && paymentAccountKeystore.hasOwnProperty('_privKey')) {\n console.warn('* deprecated API * - job.exec invoked with ethereum wallet object as paymentAccountKeystore') /* /wg oct 2019 */\n paymentAccountKeystore = paymentAccountKeystore._privKey\n }\n /** XXX @todo deprecate use of private keys */\n if (wallet.isPrivateKey(paymentAccountKeystore)) {\n console.warn('* deprecated API * - job.exec invoked with private key as paymentAccountKeystore') /* /wg dec 2019 */\n paymentAccountKeystore = await new wallet.Keystore(paymentAccountKeystore, '');\n }\n\n this.setPaymentAccountKeystore(paymentAccountKeystore)\n }\n\n // Unlock\n if (this[INTERNAL_SYMBOL].paymentAccountKeystore) {\n // Throws if they fail to unlock, we allow this since the keystore was set programmatically. \n await this[INTERNAL_SYMBOL].paymentAccountKeystore.unlock(undefined, parseFloat(dcpConfig.job.maxDeployTime));\n } else {\n // If not set programmatically, we keep trying to get an unlocked keystore ... forever.\n let locked = true;\n let safety = 0; // no while loop shall go unguarded\n let ks;\n do {\n ks = null;\n // custom message for the browser modal to denote the purpose of keystore submission\n let msg = `This application is requesting a keystore file to execute ${this.public.description || this.public.name || 'this job'}. Please upload the corresponding keystore file. If you upload a keystore file which has been encrypted with a passphrase, the application will not be able to use it until it prompts for a passphrase and you enter it.`;\n try {\n ks = await wallet.get({ contextId: this.contextId, jobName: this.public.name, msg});\n } catch (e) {\n if (e.code !== ClientModal.CancelErrorCode) throw e;\n };\n if (ks) {\n try {\n await ks.unlock(undefined, parseFloat(dcpConfig.job.maxDeployTime));\n locked = false;\n } catch (e) {\n const expectedCodes = [wallet.unlockFailErrorCode, ClientModal.CancelErrorCode];\n if (!expectedCodes.includes(e.code)) throw e;\n }\n }\n if (safety++ > 1000) throw new Error('EINFINITY: job.exec tried wallet.get more than 1000 times.')\n } while (locked);\n this.setPaymentAccountKeystore(ks)\n }\n\n // We either have a valid keystore + password or we have rejected by this point.\n if (!this.slicePaymentOffer) {\n throw new Error('A payment profile must be assigned before executing the job');\n } else {\n let pd = this[INTERNAL_SYMBOL].payloadDetails;\n pd.feeStructure = this[INTERNAL_SYMBOL].slicePaymentOffer.toFeeStructure(pd.data.length);\n }\n\n if (!this.address) {\n try {\n this.readyStateChange('init');\n await this[DEPLOY_JOB]();\n // localExec jobs are not entered in any compute group.\n if (!this[INTERNAL_SYMBOL].payloadDetails.localExec) {\n // Add this job to its currently-defined compute groups (if any)\n let failedComputeGroups = 0;\n for (const group of this.computeGroups) {\n try\n {\n await computeGroups.addJob(this.address, group);\n this.emit('addComputeGroup', group);\n } catch (error)\n {\n failedComputeGroups++;\n error.message = `Error from compute group with joinKey '${group.joinKey}': ` + error.message;\n this.emit('error', error)\n }\n }\n if (this.computeGroups.length > 0 && failedComputeGroups === this.computeGroups.length)\n {\n this.emit('error', 'Failed to join all specified compute groups, cancelling the job')\n this.cancel('No valid compute groups.');\n }\n\n computeGroups\n .closeServiceConnection()\n .catch((err) =>\n console.error(\n 'Warning: could not close compute groups service connection',\n err,\n ),\n );\n }\n\n this.readyState = 'deployed';\n this.emit('readystatechange', this.readyState);\n this.emit('accepted');\n } catch (error) {\n if (ON_BROWSER) {\n await ClientModal.alert(error, { title: 'Failed to deploy job!' });\n }\n\n throw error;\n }\n } else {\n await this[ADD_LISTENERS]();\n\n this.readyState = 'reconnected';\n this.emit('readystatechange', this.readyState);\n }\n\n this[ON_STATUS](this[INTERNAL_SYMBOL].payloadDetails.status);\n this[INTERNAL_SYMBOL].connected = true;\n\n return new Promise((resolve, reject) => {\n const onComplete = () => resolve(this.results);\n const onCancel = (event) => {\n const reason = `Job was cancelled by ${\n event.reason ? event.reason : 'Cancelled for unknown reason'\n }.`;\n\n /**\n * FIXME(DCP-1150): Remove this since normal cancel event is noisy\n * enough to not need stopped event too.\n */\n if (ON_BROWSER && !this[INTERNAL_SYMBOL].listeningForCancel)\n this[INTERNAL_SYMBOL].cancelAlert(event.reason);\n this.emit('cancel', event);\n reject(new DCPError(event.reason, event.code));\n };\n\n this[INTERNAL_SYMBOL].events.once('stopped', (stopEvent) => {\n switch (stopEvent.runStatus) {\n case 'finished':\n onComplete();\n break;\n case '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 'cancelled', 'finished', and 'paused'.\n */\n reject(\n new Error(\n `Unknown event \"${stopEvent.runStatus}\" caused the job to be stopped.`,\n ),\n );\n break;\n }\n });\n\n if (!this[INTERNAL_SYMBOL].payloadDetails.running) {\n const runStatus = this[INTERNAL_SYMBOL].payloadDetails.runStatus;\n this[INTERNAL_SYMBOL].events.emit('stopped', { runStatus });\n }\n })\n .finally(() => {\n const handleErr = (e) => {\n console.error('Error while closing job connection:');\n console.error(e);\n }\n\n // Create an async IIFE to not block the promise chain\n (async () => {\n //delay to let last few events to be received\n await new Promise((resolve) => setTimeout(resolve, 1000));\n \n // close all of the connections so that we don't cause node processes to hang.\n await this.eventSubscriber.close().catch(handleErr);\n this.deployConnection.off('close', this.openDeployConn)\n await this.deployConnection.close().catch(handleErr);\n })();\n });\n }\n\n /**\n * job.addListeners(): Private function used to set up event listeners to the scheduler\n * before deploying the job.\n */\n async [ADD_LISTENERS] () {\n // This is important: We need to flush the task queue before adding listeners\n // because we queue pending listeners by listening to the newListener event (in the constructor).\n // If we don't flush here, then the newListener events may fire after this function has run,\n // and the events won't be properly set up.\n await new Promise(resolve => setTimeout(resolve, 0));\n\n // @todo: Listen for an estimated cost, probably emit an \"estimated\" event when it comes in?\n // also @todo: Do the estimation task(s) on the scheduler and send an \"estimated\" event\n\n let listeningForResults = false;\n let listenForResults = async () => {\n listeningForResults = true;\n // Initialize the local results arr, don't replace! Modify in-place only because the result handle needs it\n this[INTERNAL_SYMBOL].results.length = this[INTERNAL_SYMBOL].resultsAvailable.length = this[INTERNAL_SYMBOL].payloadDetails.data.length;\n\n // Listen for result events\n await this.eventSubscriber.subscribe('job::result', (ev) => this[ON_RESULT](ev), {\n filter: { job: this.address, }\n });\n }\n\n if (this.listenerCount('result') > 0) {\n await listenForResults();\n } else {\n this.on('newListener', (evt) => {\n if (evt === 'result' && !listeningForResults) {\n listenForResults();\n }\n });\n }\n\n // Listen for status updates\n // e.g. when a slice gets distributed or finishes being computed\n await this.eventSubscriber.subscribe('job::status', (ev) => this[ON_STATUS](ev), {\n filter: { job: this.address, }\n });\n\n // Listen for the job stopping (complete) - used to reject the exec promise\n await this.eventSubscriber.subscribe('job::stop', (ev) => this[INTERNAL_SYMBOL].events.emit('stopped', ev), {\n filter: { job: this.address, },\n });\n // Listen for the job being cancelled - used to reject the exec promise\n await this.eventSubscriber.subscribe('job::cancel', (ev) => this[INTERNAL_SYMBOL].events.emit('stopped', ev), {\n filter: { job: this.address, },\n });\n\n /**\n * Listen for the job ENOFUNDS (the payment account has run out) - used to\n * pause the exec promise.\n */\n await this.eventSubscriber.subscribe(\n 'job::ENOFUNDS',\n (event) => this.emit('nofunds', event),\n {\n filter: { job: this.address },\n },\n );\n\n // Connect listeners that were set up before exec\n const evts = Array.from(this[INTERNAL_SYMBOL].subscribedEvents);\n this[INTERNAL_SYMBOL].subscribedEvents.clear();\n for (let evt of evts) {\n await this[LISTEN_TO_EVENTS](evt);\n }\n\n if (!this[INTERNAL_SYMBOL].listeningForError) {\n await this[LISTEN_TO_EVENTS]('error');\n }\n\n // Connect listeners that are set up after exec\n this.on('newListener', (evt) => {\n if (evt === 'newListener') return;\n this[LISTEN_TO_EVENTS](evt);\n });\n\n // Connect work event listeners that were set up before exec\n const workEvts = Array.from(this[INTERNAL_SYMBOL].subscribedWorkerEvents);\n this[INTERNAL_SYMBOL].subscribedWorkerEvents.clear();\n for (let evt of workEvts) {\n await this[LISTEN_TO_WORK_EVENTS](evt);\n }\n\n // Connect work event listeners that are set up after exec\n this.work.on('newListener', (evt) => {\n if (evt === 'newListener') return;\n this[LISTEN_TO_WORK_EVENTS](evt);\n });\n }\n\n /**\n * Subscribes to either reliable events or optional events,\n * depending on the eventName being in RELIABLE_EVENTS or OPTIONAL_EVENTS\n * @param {string} eventName \n */\n async [LISTEN_TO_EVENTS](eventName) {\n // Listen for Reliable Events (ENOFUNDS, etc.)\n // The client app will add a listener for the eventName,\n // eg. job.on('ENOFUNDS', handleNoFunds)\n const listenForReliableEvent = async (eventName) => {\n await this.eventSubscriber.subscribe('job::reliableEvent', async (ev) => {\n let { /* eventName, - use original event name */ eventData, serializer } = ev;\n switch (serializer) {\n case 'json':\n eventData = JSON.parse(eventData);\n break;\n case 'serialize':\n eventData = serialize.deserialize(eventData);\n break;\n default:\n throw new RangeError(`Unsupported serializer: ${serializer}`)\n }\n\n if (eventName === 'complete') {\n // Complete is a special case: it should emit the ResultHandle\n this.emit(eventName, this.results);\n } else {\n this.emit(eventName, eventData);\n }\n }, {\n filter: {\n job: this.address,\n eventName: eventName.toUpperCase(),\n },\n });\n }\n\n // Establishes listeners for optional events when requested by the client\n const listenForOptionalEvent = async (eventName) => {\n let subscribedEventName = eventName;\n let callback = (ev) => this.emit(eventName, ev);\n let omit = [];\n switch (eventName) {\n case 'noProgress':\n case 'noprogress':\n omit.push('data');\n subscribedEventName = 'noProgress';\n break;\n case 'noProgressData':\n case 'noprogressdata':\n subscribedEventName = 'noProgress';\n break;\n case 'console':\n callback = (ev) => {\n ev.message = kvin.deserialize(ev.message);\n this.emit(eventName, ev);\n };\n break;\n }\n\n log('listening to optional event:', { eventName, subscribedEventName });\n await this.eventSubscriber.subscribe(`job::${subscribedEventName}`, callback, {\n filter: { job: this.address, },\n omit,\n });\n }\n\n if (this[INTERNAL_SYMBOL].subscribedEvents.has(eventName)) {\n // already subscribed to this event\n return;\n } else {\n this[INTERNAL_SYMBOL].subscribedEvents.add(eventName);\n\n if (RELIABLE_EVENTS.map(ev => ev.toLowerCase()).includes(eventName)) {\n // console.debug('599: listening for reliable event:', eventName);\n await listenForReliableEvent(eventName);\n } else if (OPTIONAL_EVENTS.map(ev => ev.toLowerCase()).includes(eventName)) {\n // console.debug('602: listening for optional event:', eventName);\n await listenForOptionalEvent(eventName);\n }\n else {\n // console.debug('606: listening for unexpected/unsupported event:', eventName);\n }\n }\n }\n\n /**\n * Establishes listeners for worker events when requested by the client\n * @param {string} eventName \n */\n async [LISTEN_TO_WORK_EVENTS](eventName) {\n if (this[INTERNAL_SYMBOL].subscribedWorkerEvents.has(eventName)) {\n // already subscribed to this event\n return;\n } else {\n this[INTERNAL_SYMBOL].subscribedWorkerEvents.add(eventName);\n\n await this.eventSubscriber.subscribe('job::custom', (ev) => this.work.emit(eventName, ev), {\n filter: {\n job: this.address,\n eventName,\n }\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 [ON_RESULT] (ev) {\n if (this[INTERNAL_SYMBOL].results === null) {\n // This should never happen - the onResult event should only be established/called\n // in addListeners which should also initialize the internal results array\n throw new Error('Job.onResult was invoked before initializing internal results');\n }\n \n const { result: _result, time } = ev.result;\n let result = await fetchURI(_result);\n try {\n result = kvin.unmarshal(result)\n } catch (error) {\n if (error.message === \"Invalid serialization format\"){\n //Means the slice was returned from a web-worker, which doesn't double-serialize the results like the sa-worker does\n }\n else {\n throw error\n }\n }\n\n if (this[INTERNAL_SYMBOL].results[ev.sliceNumber]) {\n const changed = JSON.stringify(this[INTERNAL_SYMBOL].results[ev.sliceNumber]) !== JSON.stringify(result);\n this.emit('duplicate-result', { sliceNumber: ev.sliceNumber, changed });\n }\n\n this[INTERNAL_SYMBOL].results[ev.sliceNumber] = result;\n this[INTERNAL_SYMBOL].resultsAvailable[ev.sliceNumber] = true;\n this.emit('result', { sliceNumber: ev.sliceNumber, result });\n this.emit('resultsUpdated');\n }\n\n /**\n * Receives status events from the scheduler, updates the local status object\n * and emits a 'status' event\n *\n * @param {object} ev - the status event received from\n * protocol.listen('/status/0xThisGenAdr')\n * @param {boolean} emitStatus - value indicating whether or not the status\n * event should be emitted\n */\n [ON_STATUS]({ runStatus, total, distributed, computed }, emitStatus = true) {\n Object.assign(this[INTERNAL_SYMBOL].status, {\n runStatus,\n total,\n distributed,\n computed,\n });\n\n if (emitStatus) {\n this.emit('status', this.status);\n }\n }\n\n /**\n * Sends a request to the scheduler to deploy the job.\n */\n async [DEPLOY_JOB] () {\n const { payloadDetails } = this[INTERNAL_SYMBOL];\n \n this[SNAPSHOT](); /* .payloadDetails now up to date */\n \n /* Send sideloader bundle to the package server */\n if (DCP_ENV.platform === 'nodejs' && this[INTERNAL_SYMBOL].dependencies.length) {\n let {pkg, unresolved} = await this._publishLocalModules();\n\n payloadDetails.dependencies = unresolved;\n if (pkg)\n payloadDetails.dependencies.push(pkg.name + '/' + sideloaderModuleIdentifier);\n }\n \n this.readyState = 'preauth';\n this.emit('readystatechange', this.readyState);\n\n /* eagerly connect to dependent services for better performance */\n computeGroups.keepAlive()\n \n if (!this.deployConnection || !this.deployConnection.state.is('established')) {\n try {\n await this.deployConnection.connect();\n }\n catch (error) {\n console.error('1046: connection failed', error);\n throw error;\n }\n }\n \n const adhocId = payloadDetails.uuid.slice(payloadDetails.uuid.length - 6, payloadDetails.uuid.length);\n const schedId = this.deployConnection.peerAddress;\n const myId = this.deployConnection.identity;\n const preauthToken = await bankUtil.preAuthorizePayment(schedId, payloadDetails.maxDeployPayment, this.paymentAccountKeystore);\n const { dataRange, dataValues, dataPattern } = marshalInputData(payloadDetails.data);\n\n this.readyState = 'deploying';\n this.emit('readystatechange', this.readyState);\n \n /* Payload format is documented in scheduler-v4/libexec/job-submit/operations/submit.js */\n const submitPayload = {\n owner: myId.address,\n paymentAccount: this.paymentAccountKeystore.address,\n priority: 0, // @nyi\n\n workFunctionURI: payloadDetails.workFunctionURI,\n uuid: payloadDetails.uuid,\n mvScalarSlicePayment: +payloadDetails.feeStructure.marketValue || 0, // @todo: improve feeStructure internals to better reflect v4\n absoluteSlicePayment: +payloadDetails.feeStructure.maxPerRequest || 0,\n requirePath: payloadDetails.requirePath,\n modulePath: payloadDetails.modulePath,\n dependencies: payloadDetails.dependencies,\n requirements: payloadDetails.requirements, /* capex */\n localExec: payloadDetails.localExec,\n\n description: payloadDetails.public.description || 'Discreetly making the world smarter',\n name: payloadDetails.public.name || 'Ad-Hoc Job' + adhocId,\n \n preauthToken, // XXXwg/er @todo: validate this after fleshing out the stub(s)\n\n resultStorageType: payloadDetails.resultStorageType, // @todo: implement other result types\n resultStorageDetails: payloadDetails.resultStorageDetails, // Content depends on resultStorageType\n resultStorageParams: payloadDetails.resultStorageParams, // Post params for off-prem storage\n dataRange,\n marshalizedDataValues: kvin.marshal(dataValues), // serialize input data to avoid passing them as json\n dataPattern,\n };\n\n /* Determine thee type of the arguments option and set the submit message payload accordingly. */\n if (payloadDetails.arguments instanceof URL || payloadDetails.arguments instanceof DcpURL) {\n submitPayload.argumentsURI = payloadDetails.arguments.href;\n } else if (payloadDetails.arguments) {\n try {\n submitPayload.marshalizedArguments = kvin.marshal(Array.from(payloadDetails.arguments));\n } catch(e) {\n throw new Error(`Could not convert job arguments to Array (${e.message})`);\n }\n }\n\n // XXXpfr Excellent tracing.\n if (debugging('dcp-client')) {\n dumpObject(submitPayload, 'Submit: Job Index: Examine submitPayload', 128);\n }\n\n // Deploy the job!\n const deployed = await this.deployConnection.send('submit', submitPayload, myId);\n\n if (!deployed.success) {\n if(deployed.payload.code === 'ENOTFOUND') {\n throw new DCPError(`Failed to submit job to scheduler. Account: ${submitPayload.paymentAccount} was not found or does not have sufficient balance (${deployed.payload.info.deployCost} DCCs needed to deploy this job)`, deployed.payload);\n } else {\n throw new DCPError('Failed to submit job to scheduler', deployed.payload);\n }\n }\n\n this.address = payloadDetails.address = deployed.payload.job;\n this[INTERNAL_SYMBOL].deployCost = deployed.payload.deployCost;\n \n if (!payloadDetails.status)\n payloadDetails.status = {\n runStatus: null,\n total: 0,\n computed: 0,\n distributed: 0,\n };\n \n payloadDetails.runStatus = payloadDetails.status.runStatus = deployed.payload.status;\n payloadDetails.status.total = deployed.payload.lastSliceNumber;\n payloadDetails.running = true;\n \n this.readyState = 'listeners';\n\n this.emit('readystatechange', this.readyState);\n\n const listenersP = this[ADD_LISTENERS]();\n\n // @todo: NYI, but we'll need it _soon_:\n // const { payload: fetched } = await this.schedulerConnection.send('fetchJob', {\n // job: this.address,\n // owner: this.paymentAccountKeystore.address,\n // includeData: false, // we already have the data, don't bother sending it back\n // }, this.paymentAccountKeystore);\n\n this[INTERNAL_SYMBOL].payloadDetails = {\n ...this[INTERNAL_SYMBOL].payloadDetails,\n ...payloadDetails,\n };\n \n return listenersP;\n }\n\n /**\n * This function is identical to exec, except that the job is executed locally\n * in the client.\n * @async\n * @param {number} cores - the number of local cores in which to execute the job.\n * @param {...any} args - The remaining arguments are identical to the arguments of exec\n * @return {Promise<ResultHandle>} - resolves with the results of the job, rejects on an error\n * @access public\n */\n localExec (cores = 1, ...args) {\n this[INTERNAL_SYMBOL].payloadDetails.localExec = true;\n\n let worker;\n this.on('accepted', () => {\n // Start a worker for this job\n worker = new Worker({\n localExec: true,\n jobAddresses: [this.address],\n paymentAddress: this[INTERNAL_SYMBOL].paymentAccountKeystore.address,\n maxWorkingSandboxes: cores,\n sandboxOptions: {\n ignoreNoProgress: true,\n SandboxConstructor: (DCP_ENV.platform === 'nodejs' &&\n __webpack_require__(/*! ../worker/evaluators */ \"./src/dcp-client/worker/evaluators/index.js\").nodeEvaluatorFactory())\n },\n });\n\n worker.start().catch((e) => {\n console.error(\"Failed to start worker for localExec:\");\n console.error(e.message);\n });\n });\n\n return this.exec(...args).finally(() => {\n if (worker) {\n setTimeout(() => {\n // stop the worker\n worker.stop(true);\n }, 3000);\n }\n });\n }\n\n /**\n * The current job status. Will have undefined values when the handle hasn't had exec called on it yet.\n * @access public\n * @readonly\n * @type {object}\n * @property {number} total Total number of slices in the job\n * @property {number} distributed Number of slices that have been distributed\n * @property {number} computed Number of slices that have completed execution (returned a result)\n * @property {string} runStatus Current runStatus of the job\n */\n get status () {\n // Shallow-copy to prevent modification\n return { ...this[INTERNAL_SYMBOL].status };\n }\n\n get requirePath() {\n return this[INTERNAL_SYMBOL].requirePath;\n }\n\n /**\n * This function specifies a module dependency (when the argument is a string)\n * or a list of dependencies (when the argument is an array) of the work\n * function. This function can be invoked multiple times before deployment.\n * @param {string | string[]} modulePaths - A string or array describing one\n * or more dependencies of the job.\n * @access public\n */\n requires(modulePaths) {\n if (\n typeof modulePaths !== 'string' &&\n (!Array.isArray(modulePaths) ||\n modulePaths.some((modulePath) => typeof modulePath !== 'string'))\n ) {\n throw new TypeError(\n 'The argument to dependencies is not a string or an array of strings',\n );\n } else if (modulePaths.length === 0) {\n throw new RangeError(\n 'The argument to dependencies cannot be an empty string or array',\n );\n } else if (\n Array.isArray(modulePaths) &&\n modulePaths.some((modulePath) => modulePath.length === 0)\n ) {\n throw new RangeError(\n 'The argument to dependencies cannot be an array containing an empty string',\n );\n }\n\n if (!Array.isArray(modulePaths)) {\n modulePaths = [modulePaths];\n }\n\n for (const modulePath of modulePaths) {\n if (modulePath[0] !== '.' && modulePath.indexOf('/') !== -1) {\n const modulePrefixRegEx = /^(.*)\\/.*?$/;\n const [, modulePrefix] = modulePath.match(modulePrefixRegEx);\n if (\n modulePrefix &&\n this[INTERNAL_SYMBOL].requirePath.indexOf(modulePrefix) === -1\n ) {\n this[INTERNAL_SYMBOL].requirePath.push(modulePrefix);\n }\n }\n\n this[INTERNAL_SYMBOL].dependencies.push(modulePath);\n }\n }\n\n get slicePaymentOffer () {\n return this[INTERNAL_SYMBOL].slicePaymentOffer;\n }\n\n /**\n * The keystore that will be used to pay for the job. Can be set with {@link Job.setPaymentAccountKeystore} or by providing a keystore to {@link Job.exec}.\n * @readonly\n * @access public\n * @type {module:dcp/wallet.AuthKeystore}\n */\n get paymentAccountKeystore () {\n return this[INTERNAL_SYMBOL].paymentAccountKeystore;\n }\n\n /** Set the account upon which funds will be drawn to pay for the job.\n * @param {module:dcp/wallet.AuthKeystore} keystore A keystore that representa a bank account.\n * @access public\n */\n setPaymentAccountKeystore (keystore) {\n if (this.address) {\n if (!keystore.address.eq(this[INTERNAL_SYMBOL].payloadDetails.owner)) {\n let message = \"Cannot change payment account after job has been deployed\";\n this.emit('EPERM', message);\n throw new Error(`EPERM: ${message}`);\n }\n }\n \n if (!(keystore instanceof wallet.Keystore)) {\n let e = new Error('Not an instance of Keystore: ' + keystore.toString())\n console.log(`Not an instance of Keystore: ${keystore}`)\n throw e\n }\n this[INTERNAL_SYMBOL].paymentAccountKeystore = keystore;\n }\n\n /** Set the slice payment offer. This is equivalent to the first argument to exec.\n * @param {number} slicePaymentOffer - The number of DCC the user is willing to pay to compute one slice of this job\n */\n setSlicePaymentOffer (slicePaymentOffer) {\n this[INTERNAL_SYMBOL].slicePaymentOffer = new SlicePaymentOffer(slicePaymentOffer);\n }\n\n /**\n * @param {URL} locationUrl - A URL object \n * @param {object} postParams - An object with any parameters that a user would like to be passed to a \n * remote result location. This object is capable of carry API keys for S3, \n * DropBox, etc. These parameters are passed as parameters in an \n * application/x-www-form-urlencoded request.\n */\n setResultStorage(locationUrl, postParams) {\n if (locationUrl instanceof URL || locationUrl instanceof DcpURL) {\n this[INTERNAL_SYMBOL].resultStorageDetails = locationUrl;\n } else {\n const e = new Error('Not an instance of a DCP URL: ' + locationUrl);\n console.log('Not an instance of a DCP URL ' + locationUrl);\n throw e;\n }\n\n // resultStorageParams contains any post params required for off-prem storage\n if (typeof postParams !== 'undefined' && typeof postParams === 'object' ) {\n this[INTERNAL_SYMBOL].resultStorageParams = postParams;\n } else {\n const e = new Error('Not an instance of a object: ' + postParams);\n console.log('Not an instance of an object ' + postParams);\n throw e;\n } \n\n // Some type of object here\n this[INTERNAL_SYMBOL].resultStorageType = 'pattern';\n } \n}\n\n/**\n * @typedef {object} MarshaledInputData\n * @property {any[]} [dataValues]\n * @property {string} [dataPattern]\n * @property {RangeObject} [dataRange]\n */\n\n/**\n * Depending on the shape of the job's data, resolve it into a RangeObject, a\n * Pattern, or a values array, and return it in the appropriate property.\n *\n * @param {any} data Job's input data\n * @return {MarshaledInputData} An object with one of the following properties set:\n * - dataValues: job input is an array of arbitrary values \n * - dataPattern: job input is a URI pattern \n * - dataRange: job input is a RangeObject (and/or friends)\n */\nfunction marshalInputData(data) {\n if (!(data instanceof Object\n || data instanceof SuperRangeObject)) {\n throw new TypeError(`Invalid job data type: ${typeof data}`);\n }\n\n /**\n * @type MarshaledInputData\n */\n const marshalledInputData = {};\n\n // TODO(wesgarland): Make this more robust.\n if (data instanceof SuperRangeObject ||\n (data.hasOwnProperty('ranges') && data.ranges instanceof MultiRangeObject) ||\n (data.hasOwnProperty('start') && data.hasOwnProperty('end'))) {\n marshalledInputData.dataRange = data;\n } else if (Array.isArray(data)) {\n marshalledInputData.dataValues = data;\n } else if (data instanceof URL || data instanceof DcpURL) {\n marshalledInputData.dataPattern = String(data);\n }\n\n log('marshalledInputData:', marshalledInputData);\n return marshalledInputData;\n}\n\nObject.assign(exports, {\n Job,\n SlicePaymentOffer,\n ResultHandle,\n});\n\n\n//# sourceURL=webpack:///./src/dcp-client/job/index.js?");
5102
+ eval("/**\n * @file job/index.js\n * @author Eddie Roosenmaallen, eddie@kingsds.network\n * Matthew Palma, mpalma@kingsds.network\n * @date November 2018\n *\n * This module implements the Compute API's Job Handle\n *\n */\n\n/** @typedef {import('dcp/dcp-client/wallet/keystore').Keystore} Keystore */\n\n\nconst BigNumber = __webpack_require__(/*! bignumber.js */ \"./node_modules/bignumber.js/bignumber.js\");\nconst { v4: uuidv4 } = __webpack_require__(/*! uuid */ \"./node_modules/uuid/dist/esm-browser/index.js\");\nconst { EventEmitter, PropagatingEventEmitter } = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\");\nconst { RangeObject, MultiRangeObject, DistributionRange, SuperRangeObject } = __webpack_require__(/*! dcp/dcp-client/range-object */ \"./src/dcp-client/range-object.js\");\nconst { fetchURI, encodeDataURI, dumpObject } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\nconst serialize = __webpack_require__(/*! dcp/utils/serialize */ \"./src/utils/serialize.js\");\nconst wallet = __webpack_require__(/*! dcp/dcp-client/wallet */ \"./src/dcp-client/wallet/index.js\");\nconst protocolV4 = __webpack_require__(/*! dcp/protocol-v4 */ \"./src/protocol-v4/index.js\");\nconst { EventSubscriber } = __webpack_require__(/*! dcp/events/event-subscriber */ \"./src/events/event-subscriber.js\");\nconst { DcpURL } = __webpack_require__(/*! dcp/common/dcp-url */ \"./src/common/dcp-url.js\");\nconst ClientModal = __webpack_require__(/*! dcp/dcp-client/client-modal */ \"./src/dcp-client/client-modal/index.js\");\nconst { Worker } = __webpack_require__(/*! dcp/dcp-client/worker */ \"./src/dcp-client/worker/index.js\");\nconst { RemoteDataSet } = __webpack_require__(/*! dcp/dcp-client/remote-data-set */ \"./src/dcp-client/remote-data-set.js\");\nconst { RemoteDataPattern } = __webpack_require__(/*! dcp/dcp-client/remote-data-pattern */ \"./src/dcp-client/remote-data-pattern.js\");\nconst { ResultHandle } = __webpack_require__(/*! ./result-handle */ \"./src/dcp-client/job/result-handle.js\");\nconst { SlicePaymentOffer } = __webpack_require__(/*! ./slice-payment-offer */ \"./src/dcp-client/job/slice-payment-offer.js\");\nconst DCP_ENV = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\nconst dcpPublish = __webpack_require__(/*! dcp/common/dcp-publish */ \"./src/common/dcp-publish.js\");\nconst computeGroups = __webpack_require__(/*! dcp/dcp-client/compute-groups */ \"./src/dcp-client/compute-groups/index.js\");\nconst schedulerConstants = __webpack_require__(/*! dcp/common/scheduler-constants */ \"./src/common/scheduler-constants.js\");\nconst bankUtil = __webpack_require__(/*! dcp/dcp-client/bank-util */ \"./src/dcp-client/bank-util.js\");\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\nconst debugging = __webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope('dcp-client');\nconst kvin = __webpack_require__(/*! kvin */ \"./node_modules/kvin/kvin.js\");\n\nconst log = (...args) => {\n if (debugging('job')) {\n console.debug('dcp-client:job', ...args);\n }\n};\n\nconst ON_BROWSER = DCP_ENV.isBrowserPlatform;\nconst sideloaderModuleIdentifier = 'sideloader-v1';\n\n// Symbols used to hide private members and functions on the Job instance\nconst debugBuild = (__webpack_require__(/*! dcp/common/dcp-build */ \"./src/common/dcp-build.js\").build === 'debug');\nconst INTERNAL_SYMBOL = debugBuild ? '_' : Symbol('Job Internals');\nconst SNAPSHOT = debugBuild ? '_snapshot' : Symbol('Job.snapshot');\nconst DEPLOY_JOB = debugBuild ? '_deploy' : Symbol('Job.deploy');\n\nconst ADD_LISTENERS = Symbol('Job.addListeners');\nconst LISTEN_TO_EVENTS = Symbol('Job.listenToEvents');\nconst LISTEN_TO_WORK_EVENTS = Symbol('Job.listenToWorkEvents');\nconst ON_RESULT = Symbol('Job.onResult');\nconst ON_STATUS = Symbol('Job.onStatus');\n\nexports.JOB_INTERNAL_SYMBOL = INTERNAL_SYMBOL; /* allow friends to access our guts, eg. job/result-handle */\n\nconst DEFAULT_REQUIREMENTS = {\n engine: {\n es7: null,\n spidermonkey: null\n },\n environment: {\n webgpu: null,\n offscreenCanvas: null,\n fdlibm: null\n },\n browser: {\n chrome: null\n },\n details: {\n offscreenCanvas: {\n bigTexture4096: null,\n bigTexture8192: null,\n bigTexture16384: null,\n bigTexture32768: null,\n }\n },\n discrete: null,\n};\nconst ZERO_COST_HOLD_ADDRESS = '0x' + '0'.repeat(130);\n\n/** @typedef {import('../range-object').RangeLike} RangeLike */\n\n/**\n * Ensure input data is an appropriate format\n * @param {RangeObject | DistributionRange | RemoteDataSet | Array | Iterable}\n * inputData - A URI-shaped string, a [Multi]RangeObject-constructing value, or\n * an array of slice data\n * @return {RangeObject | RangeLike | DistributionRange | RemoteDataSet | Array}\n * The coerced input in an appropriate format ([Multi]RangeObject,\n * DistributionRange, RemoteDataSet, or array)\n */\nconst wrangleData = (inputData) => {\n if (typeof inputData === 'object' && !!inputData.ranges) { return new MultiRangeObject(inputData) }\n\n if (RangeObject.isRangelike(inputData)) { return inputData }\n if (RangeObject.isRangeObject(inputData)) { return inputData }\n if (DistributionRange.isDistribution(inputData)) { return inputData }\n if (RangeObject.isProtoRangelike(inputData)) { return new RangeObject(inputData) }\n if (DistributionRange.isProtoDistribution(inputData)) { return new DistributionRange(inputData) }\n if (RemoteDataSet.isRemoteDataSet(inputData)) { return inputData }\n if (RemoteDataPattern.isRemoteDataPattern(inputData)) { return inputData }\n\n return Array.isArray(inputData) ? inputData : [inputData];\n};\n\n// Used to validate the requirements object,\n// applies the default requirements schema\nconst applyObjectSchema = (obj, schema, errContext='', scope='') => {\n let checkedObjs = [];\n\n for (let p in schema) {\n let fullPropScope = scope.concat(p);\n if (!(p in obj)) {\n if (typeof schema[p] === 'object' && schema[p] !== null) {\n obj[p] = {};\n checkedObjs.push(fullPropScope);\n applyObjectSchema(obj[p], schema[p], errContext, fullPropScope.concat('.'));\n } else obj[p] = schema[p];\n } else if (typeof schema[p] === 'object' && schema[p] !== null && !checkedObjs.includes(fullPropScope)) {\n if (typeof obj[p] !== 'object') throw new Error(`${errContext}: Schema mismatch, property '${fullPropScope}' should be an object.`);\n else {\n checkedObjs.push(fullPropScope);\n applyObjectSchema(obj[p], schema[p], errContext, fullPropScope.concat('.'));\n }\n } else if ((typeof schema[p] !== 'object' || schema[p] === null)\n && typeof obj[p] !== 'boolean' && obj[p] !== null) {\n throw new Error(`${errContext}: Schema mismatch, object property '${fullPropScope}' should be a boolean.`);\n }\n }\n\n for (let p in obj) {\n let fullPropScope = scope.concat(p);\n if (!(p in schema)) throw new Error(`${errContext}: Schema mismatch, object has extra key '${fullPropScope}'.`);\n else if (typeof obj[p] === 'object' && obj[p] !== null && !checkedObjs.includes(fullPropScope)) {\n checkedObjs.push(fullPropScope);\n applyObjectSchema(obj[p], schema[p], errContext, fullPropScope.concat('.'));\n }\n }\n}\n\n/**\n * @classdesc The Compute API's Job Handle (see {@link https://docs.dcp.dev/specs/compute-api.html#job-handles|Compute API spec})\n * Job handles are objects which correspond to jobs. \n * They are created by some exports of the compute module, such as {@link module:dcp/compute.do|compute.do} and {@link module:dcp/compute.for|compute.for}.\n * @extends module:dcp/dcp-events.PropagatingEventEmitter\n * @hideconstructor\n * @access public\n */\nclass Job extends PropagatingEventEmitter {\n /**\n * This event is emitted when the job is accepted by the scheduler on deploy.\n * \n * @event Job#accepted\n * @access public\n * @type {object}\n * @property {object} job Original object that was delivered to the scheduler for deployment\n *//**\n * Fired when the job is cancelled.\n * \n * @event Job#cancel\n * @access public\n *//**\n * Fired when a result is returned.\n * \n * @event Job#result\n * @access public\n * @type {object}\n * @property {string} jobAddress Address of the job\n * @property {string} task ID of the task (slice) the result came from\n * @property {number} sort The index of the slice\n * @property {object} result\n * @property {string} result.request\n * @property {*} result.result The value returned from the work function\n *//**\n * Fired when the result handle is modified, either when a new `result` event is fired or when the results are populated with `results.fetch()`\n * \n * @event Job#resultsUpdated\n * @access public\n *//**\n * Fired when the job has been completed.\n * \n * @event Job#complete\n * @access public\n * @type {ResultHandle}\n *//**\n * Fired when the job's status changes.\n * \n * @event Job#status\n * @access public\n * @type {object}\n * @property {string} jobAddress Address of the job\n * @property {number} total Total number of slices in the job\n * @property {number} distributed Number of slices that have been distributed\n * @property {number} computed Number of slices that have completed execution (returned a result)\n * @property {string} runStatus Current runStatus of the job\n *//**\n * Fired when a slice throws an error.\n * \n * @event Job#error\n * @access public\n * @type {object}\n * @property {string} jobAddress Address of the job\n * @property {number} sliceIndex Index of the slice that threw the error\n * @property {string} message The error message\n * @property {string} stack The error stacktrace\n * @property {string} name The error type name\n *//**\n * Fired when a slice uses one of the console log functions.\n * \n * @event Job#console\n * @access public\n * @type {object}\n * @property {string} jobAddress Address of the job\n * @property {number} sliceIndex The index of the slice that produced this event\n * @property {string} level The log level, one of `debug`, `info`, `log`, `warn`, or `error`\n * @property {string} message The console log message\n *//**\n * Fired when a slice is stopped for not calling progress. Contains information about how long the slice ran for, and about the last reported progress calls.\n * \n * @event Job#noProgress\n * @access public\n * @type {object}\n * @property {string} jobAddress Address of the job\n * @property {number} sliceIndex The index of the slice that failed due to no progress\n * @property {number} timestamp How long the slice ran before failing\n * @property {object} progressReports\n * @property {object} progressReports.last The last progress report received from the worker\n * @property {number} progressReports.last.timestamp Time since the start of the slice\n * @property {number} progressReports.last.progress Progress value reported\n * @property {*} progressReports.last.value The last value that was passed to the progress function\n * @property {number} progressReports.last.throttledReports Number of calls to progress that were throttled since the last report\n * @property {object} progressReports.lastUpdate The last determinate (update to the progress param) progress report received from the worker\n * @property {number} progressReports.lastUpdate.timestamp\n * @property {number} progressReports.lastUpdate.progress\n * @property {*} progressReports.lastUpdate.value\n * @property {number} progressReports.lastUpdate.throttledReports\n *//**\n * Identical to `noProgress`, except that it also contains the data that the slice was executed with.\n * \n * @event Job#noProgressData\n * @access public\n * @type {object}\n * @property {*} data The data that the slice was executed with\n *//**\n * Fired when the job is paused due to running out of funds. The job can be resumed by escrowing more funds then resuming the job.\n * \n * Event payload is the estimated funds required to complete the job\n * \n * @event Job#ENOFUNDS\n * @access public\n * @type {BigNumber}\n *//**\n * Fired when the job is cancelled due to the work function not calling the `progress` method frequently enough.\n * \n * @event Job#ENOPROGRESS\n * @access public\n *//**\n * The job was cancelled because scheduler has determined that individual tasks in this job exceed the maximum allowable execution time.\n * \n * @event Job#ESLICETOOSLOW\n * @access public\n *//**\n * Fired when the job is cancelled because too many work functions are terminating with uncaught exceptions.\n * \n * @event Job#ETOOMANYERRORS\n * @access public\n */\n\n /**\n * @form1 new Job(job_shaped_object)\n * @form2 new Job('application_worker_address'[, data[, arguments]])\n * @form3b new Job('worker source'[, data[, arguments]])\n * @form3b new Job(worker_function[, data[, arguments]])\n */\n\n constructor() {\n super('Job');\n\n this.readyStateChange = (readyState) => {\n this.readyState = readyState;\n this.emit('readyStateChange', this.readyState);\n };\n this.readyStateChange('new');\n \n /*\n * Private members\n */\n this[INTERNAL_SYMBOL] = {\n events: new EventEmitter('Job Internal'),\n connected: false, // set to true after first call to exec\n /**\n * This object holds details for generating DCPv4 messages about this job.\n * It is updated everytime we call SNAPSHOT.\n */\n payloadDetails: {\n localExec: false,\n },\n\n /**\n * The slicePaymentOffer default value is set to compute.marketValue, in .exec() \n */\n slicePaymentOffer: null,\n paymentAccountKeystore: null,\n\n /**\n * These are private but getters are provided so they can be modified but\n * not replaced.\n */\n /**\n * List of module prefixes using in CommonJS module resolution.\n * @type {string[]}\n */\n requirePath: [],\n\n /**\n * List of modules the job needs.\n * @type {string[]}\n */\n\n dependencies: [],\n\n // This array contains the names of worker events that\n // had listeners registered before exec is called, once\n // the job has been deployed then the proper event handlers\n // will be generated from this list\n subscribedEvents: new Set(),\n subscribedWorkerEvents: new Set(),\n\n results: [],\n resultsAvailable: [],\n resultStorageType: 'values',\n resultStorageDetails: undefined,\n resultStorageParams: undefined, //Holds the POST params and URL for off-prem storage\n\n // Tracks job progress\n status: {\n runStatus: null,\n total: null,\n distributed: null,\n computed: null,\n },\n\n // Cancel is special. We need to fire an `alert` when the job is canceled. \n // If they are listening for the (reliable) event then they need to be able to\n // prevent it. If not, then it'll be handled by the `exec` rejection via the 'stopped'\n // event. The result is that we want only one of two ways the `alert` can be fired\n // to be active based on whether or not the user is listening for cancel. \n // Once DCP-1150 lands, we won't need to listen on stopped since more failures will fire a cancel event.\n listeningForCancel: false,\n // TODO - cancel events should have more info in them. DCP-1150\n cancelAlert: () => ClientModal.alert(\"More details in console...\", {title: 'Job Canceled'}),\n\n listeningForError: false,\n errorAlert: (err) => ClientModal.alert(err, {title: 'Unexpected Error'}),\n\n listeningForNoFunds: false,\n noFundsAlert: (event) => {\n let msg = `Job \"${event.name}\" is paused due to insufficient funds. ${event.fundsRequired} DCC is required to compute remaining ${event.remainingSlices} slices.\\njobId: ${event.job}\\nbankAccount: ${event.bankAccount}`; \n ClientModal.alert(msg, { title: 'Job paused' })\n },\n };\n\n /*\n * Public members\n */\n // Deep copy the default requirements. I know, I hate it too\n /**\n * An object describing the requirements that workers must have to be eligible for this job. See\n * {@link https://docs.dcp.dev/specs/compute-api.html#requirements-objects|Requirements Objects}.\n *\n * @type {object}\n * @access public\n */\n this.requirements = JSON.parse(JSON.stringify(DEFAULT_REQUIREMENTS));\n this.schedulerURL = undefined;\n this.bankURL = undefined;\n this.deployURL = '';\n this.collateResults = true;\n this.listeningForResults = false;\n /**\n * @see {@link https://kingsds.atlassian.net/browse/DCP-1475?atlOrigin=eyJpIjoiNzg3NmEzOWE0OWI4NGZkNmI5NjU0MWNmZGY2OTYzZDUiLCJwIjoiaiJ9|Jira Issue}\n */\n let uuid = uuidv4();\n\n /**\n * An object describing the cost the user believes each the average slice will incur, in terms of CPU/GPU and I/O.\n * If defined, this object is used to provide initial scheduling hints and to calculate escrow amounts.\n *\n * @type {object}\n * @access public\n */\n this.initialSliceProfile = undefined;\n\n /**\n * A place to store public-facing attributes of the job. Anything stored on this object will be available inside the work \n * function (see {@link module:dcp/compute~sandboxEnv.work}). The properties documented here may be used by workers to display what jobs are currently being \n * worked on.\n * @access public\n * @property {string} name Public-facing name of this job.\n * @property {string} description Public-facing description for this job.\n * @property {string} link Public-facing link to external resource about this job.\n */\n this.public = {\n name: null,\n description: null,\n link: null,\n };\n\n this.contextId = null\n\n // The following public members are only populated after the job has been deployed\n this.address = null;\n this.receipt = null;\n this.meanSliceProfile = null;\n\n /* We avoid using job.id internally because it is easy to confuse with db::jobs.id, but is an API\n * interface that we present to end-user developers so we need to keep it.\n */\n Object.defineProperty(this, 'id', {\n get: () => this.address,\n set: (id) => this.address = id\n });\n \n // \n /**\n * An EventEmitter for custom events dispatched by the work function.\n * @type {module:dcp/dcp-events.EventEmitter}\n * @access public\n * @example\n * // in sandbox\n * work.emit('myEventName', 1, [2], \"three\");\n * // clientside\n * job.work.on('myEventName', (num, arr, string) => { });\n */\n this.work = new EventEmitter('job.work');\n\n //Initialize the eventSubscriber so each job has unique eventSubscriber\n this.eventSubscriber = new EventSubscriber(this);\n \n // Some events can't be emitted 'naturally' without having weird/wrong output.\n // An example of this is results. When results are returned from the scheduler,\n // They come in as a dataURI of kvin-ified results. We need to parse all that before\n // We actually send it to the client. For such events, we will intercept them, parse\n // them as needed, then emit the event with the 'fixed' data to the client.\n \n const ceci = this\n const parseConsole = function deserializeConsoleMessage(ev) {\n ev.message = kvin.parse(ev.message);\n ceci.emit('console', ev);\n }\n \n this.eventIntercepts = {\n result: (ev) => this[ON_RESULT](ev),\n status: (ev) => this[ON_STATUS](ev),\n cancel: (ev) => this[INTERNAL_SYMBOL].events.emit('stopped', ev),\n console: parseConsole,\n }\n\n this.eventTypes = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\").eventTypes;\n\n this.work.on('newListener', (evt) => {\n if (!this[INTERNAL_SYMBOL].connected && evt !== 'newListener') {\n this[INTERNAL_SYMBOL].subscribedWorkerEvents.add(evt);\n }\n });\n\n this.on('newListener', (evt) => {\n if (!this[INTERNAL_SYMBOL].connected && evt !== 'newListener') {\n this[INTERNAL_SYMBOL].subscribedEvents.add(evt);\n }\n });\n // Form1: If arguments[0] is an object that looks like a job, this is a 'copy constructor'\n // where we inherit as much as possible from the original instance.\n if (typeof arguments[0] === 'object' &&\n arguments[0].type &&\n arguments[0].data &&\n arguments[0].public) {\n \n let src = arguments[0];\n\n this[INTERNAL_SYMBOL].payloadDetails = {\n ...src,\n data: wrangleData(src.data), // rehydrate ranges\n };\n\n if (src.feeStructure) {\n this.setSlicePaymentOffer(src.feeStructure);\n }\n \n if (src.address)\n this.address = src.address;\n if (src.payloadData.status)\n this[ON_STATUS](this[INTERNAL_SYMBOL].payload.status, false);\n if (src.public)\n Object.assign(this.public, src.public);\n } else {\n /* Forms 2-4 */ \n if (typeof arguments[0] === 'function')\n arguments[0] = arguments[0].toString();\n\n if (typeof arguments[0] === 'string') {\n this[INTERNAL_SYMBOL].workFunctionURI = encodeDataURI(arguments[0], 'application/javascript');\n } else if (DcpURL.isURL(arguments[0])) {\n this[INTERNAL_SYMBOL].workFunctionURI = arguments[0].href;\n } \n\n const wrangledInputData = wrangleData(arguments[1] || []);\n const wrangledArguments = wrangleData(arguments[2] || []);\n \n log('wrangledInputData:', wrangledInputData);\n log('wrangledArguments:', wrangledArguments);\n \n Object.assign(this[INTERNAL_SYMBOL].payloadDetails, {\n request: 'main',\n data: wrangledInputData,\n arguments: wrangledArguments,\n });\n }\n\n // This should happen last, it depends on the this.[INTERNAL_SYMBOL].payloadDetails.data array\n /**\n * A Result Handle object used to query and manipulate the output set. \n * Present once job has been deployed.\n * @type {ResultHandle}\n * @access public\n */\n this.results = new ResultHandle(this);\n\n /**\n * Read-only copy of the job's uuid (generated or rehydrated via form1 constructor)\n */\n Object.defineProperty(this, 'uuid', {\n get: () => uuid,\n configurable: false,\n enumerable: true,\n });\n \n // each entry contains the computeGroupID, joinKey, joinSecret, joinKeystore\n // Add to public compute group by default\n this.computeGroups = [ Object.assign({}, schedulerConstants.computeGroups.public) ];\n\n\n // Initialize to null so these properties are recognized for the Job class\n this.bankConnection = null;\n this.deployConnection = null;\n this.openBankConn = function openBankConn()\n {\n ceci.bankConnection = new protocolV4.Connection(dcpConfig.bank.services.bankTeller);\n ceci.bankConnection.on('close', ceci.openBankConn);\n }\n\n this.openDeployConn = function openDeployConn()\n {\n ceci.deployConnection = new protocolV4.Connection(dcpConfig.scheduler.services.jobSubmit);\n ceci.deployConnection.on('close', ceci.openDeployConn);\n }\n\n this.openBankConn();\n this.openDeployConn();\n }\n\n /** \n * Cancel the job\n * @access public\n * @param {string} reason If provided, will be sent to client\n */\n async cancel (reason = undefined) {\n const response = await this.deployConnection.send('cancelJob', {\n job: this.address,\n ownerAddress: this.paymentAccountKeystore.address,\n reason,\n }, this.paymentAccountKeystore);\n\n return response.payload;\n }\n\n /** \n * Resume this job\n * @access public\n */\n async resume() {\n const response = await this.schedulerConnection.send('resumeJob', {\n job: this.address,\n owner: this.paymentAccountKeystore.address,\n }, this.paymentAccountKeystore);\n\n return response.payload;\n }\n\n /**\n * Helper function for retrieving info about the job. The job must have already been deployed.\n * An alias for {@link module:dcp/compute.getJobInfo}.\n * @access public\n */\n async getJobInfo(){\n return await __webpack_require__(/*! ../compute */ \"./src/dcp-client/compute.js\").compute.getJobInfo(this.address);\n }\n\n /**\n * Helper function for retrieving info about the job's slices. The job must have already been deployed.\n * An alias for {@link module:dcp/compute.getSliceInfo}.\n * @access public\n */\n async getSliceInfo(){\n return await __webpack_require__(/*! ../compute */ \"./src/dcp-client/compute.js\").compute.getSliceInfo(this.address);\n }\n \n async addSliceData(dataValues) {\n if (!Array.isArray(this[INTERNAL_SYMBOL].payloadDetails.data)) {\n throw new TypeError('Only data-by-value jobs may dynamically add slices');\n }\n\n const { payload } = await this.deployConnection.send('addSliceData', {\n job: this.address,\n dataValues,\n });\n\n if (payload.success && typeof payload.lastSliceNumber !== 'undefined')\n this.status.total = payload.lastSliceNumber;\n\n return payload;\n }\n\n /**\n * job.snapshot(): Private function used to populate the payloadDetails from private data,\n * inferred data, etc. Once this function has run, the payloadDetails are\n * considered authoritatively up to date until the calling function returns\n * or awaits.\n */\n [SNAPSHOT]() {\n const pd = this[INTERNAL_SYMBOL].payloadDetails;\n\n pd.type = 'ad-hoc'; /* @todo implement appliances */\n pd.uuid = this.uuid;\n pd.workFunctionURI = this[INTERNAL_SYMBOL].workFunctionURI;\n pd.dependencies = this[INTERNAL_SYMBOL].dependencies;\n pd.requirePath = this[INTERNAL_SYMBOL].requirePath;\n pd.modulePath = this[INTERNAL_SYMBOL].modulePath;\n pd.resultStorageType = this[INTERNAL_SYMBOL].resultStorageType;\n pd.resultStorageDetails = this[INTERNAL_SYMBOL].resultStorageDetails;\n pd.resultStorageParams = this[INTERNAL_SYMBOL].resultStorageParams;\n\n pd.requirements = this.requirements;\n applyObjectSchema(pd.requirements, DEFAULT_REQUIREMENTS, 'Requirements Object');\n \n // @todo: 'figure this out' - wise words from eddie /mp jan 2019\n if (!pd.options) { pd.options = {}; }\n if (!pd.public) { pd.public = {}; } \n\n for (let p of ['name', 'description', 'link']) {\n if (typeof this.public[p] === 'string') {\n pd.public[p] = this.public[p];\n }\n }\n\n // The max value that the client is willing to spend to deploy\n // (list on the scheduler, doesn't include compute payment)\n /// maxDeployPayment is the max the user is willing to pay to DCP (as a\n /// Hold), in addition to the per-slice offer and associated scrape.\n /// Currently calculated as `deployCost = costPerKB *\n /// (JSON.stringify(job).length / 1024) // 1e-9 per kb`\n // @todo: figure this out / er nov 2018\n pd.maxDeployPayment = 1;\n \n /// payloadDetails.timing can be provided as an initial estimate of slice time, to\n /// give a more useful useful calculated heap value (heap.value is more or less\n /// dcc-per-millisecond)\n pd.timing = pd.timing || 1; \n }\n\n /** Escrow additional funds for this job\n * @access public\n * @param {number|BigNumber} fundsRequired - A number or BigNumber instance representing the funds to escrow for this job\n */\n async escrow (fundsRequired) {\n if ((typeof fundsRequired !== 'number' && !BigNumber.isBigNumber(fundsRequired))\n || fundsRequired <= 0 || !Number.isFinite(fundsRequired) || Number.isNaN(fundsRequired)) {\n throw new Error(`Job.escrow: fundsRequired must be a number greater than zero. (not ${fundsRequired})`);\n }\n\n const response = await this.bankConnection.send('embiggenFeeStructure', {\n feeStructureAddress: this[INTERNAL_SYMBOL].payloadDetails.feeStructureId,\n additionalEscrow: BigNumber(fundsRequired),\n fromAddress: this.paymentAccountKeystore.address,\n }, this.paymentAccountKeystore);\n\n this.receipt = response.payload;\n\n return this.receipt;\n }\n\n async _pack () {\n var retval = __webpack_require__(/*! ./node-modules */ \"./src/dcp-client/job/node-modules.js\").createModuleBundle(this[INTERNAL_SYMBOL].dependencies);\n return retval;\n }\n\n /** \n * Collect all of the dependencies together, throw them into a BravoJS\n * module which sideloads them as a side effect of declaration, and transmit\n * them to the package manager. Then we return the package descriptor object,\n * which is guaranteed to have only one file in it.\n *\n * @returns {object} with properties name and files[0]\n */\n async _publishLocalModules() {\n const { tempFile, hash, unresolved } = await this._pack();\n\n if (!tempFile) {\n return { unresolved };\n }\n\n const sideloaderFilename = tempFile.filename;\n const pkg = {\n name: `dcp-pkg-v1-localhost-${hash.toString('hex')}`,\n version: '1.0.0',\n files: {\n [sideloaderFilename]: `${sideloaderModuleIdentifier}.js`,\n },\n }\n\n await dcpPublish.publish(pkg);\n tempFile.remove();\n\n return { pkg, unresolved };\n }\n\n /**\n * Deploys the job to the scheduler.\n * @param {number | object} [slicePaymentOffer=compute.marketValue] - Amount\n * in DCC that the user is willing to pay per slice.\n * @param {Keystore} [paymentAccountKeystore=wallet.get] - An instance of the\n * Wallet API Keystore that's used as the payment account when executing the\n * job.\n * @param {object} [initialSliceProfile] - An object describing the cost the\n * user believes the average slice will incur.\n * @access public\n * @emits Job#accepted\n */\n async exec(slicePaymentOffer = __webpack_require__(/*! ../compute */ \"./src/dcp-client/compute.js\").compute.marketValue, paymentAccountKeystore, initialSliceProfile) {\n if (this[INTERNAL_SYMBOL].connected) {\n throw new Error('Exec called twice on the same job handle.');\n }\n\n /* eagerly connect to depedent services for better performance */\n this.eventSubscriber.eventRouterConnection.keepalive();\n this.deployConnection.keepalive();\n\n this.readyState = 'exec'\n this.emit('readystatechange', this.readyState)\n\n if (typeof slicePaymentOffer !== 'undefined') this.setSlicePaymentOffer(slicePaymentOffer);\n if (typeof initialSliceProfile !== 'undefined') this.initialSliceProfile = initialSliceProfile;\n if (typeof paymentAccountKeystore !== 'undefined') {\n /** XXX @todo deprecate use of ethereum wallet objects */\n if (typeof paymentAccountKeystore === 'object' && paymentAccountKeystore.hasOwnProperty('_privKey')) {\n console.warn('* deprecated API * - job.exec invoked with ethereum wallet object as paymentAccountKeystore') /* /wg oct 2019 */\n paymentAccountKeystore = paymentAccountKeystore._privKey\n }\n /** XXX @todo deprecate use of private keys */\n if (wallet.isPrivateKey(paymentAccountKeystore)) {\n console.warn('* deprecated API * - job.exec invoked with private key as paymentAccountKeystore') /* /wg dec 2019 */\n paymentAccountKeystore = await new wallet.Keystore(paymentAccountKeystore, '');\n }\n\n this.setPaymentAccountKeystore(paymentAccountKeystore)\n }\n\n // Unlock\n if (this[INTERNAL_SYMBOL].paymentAccountKeystore) {\n // Throws if they fail to unlock, we allow this since the keystore was set programmatically. \n await this[INTERNAL_SYMBOL].paymentAccountKeystore.unlock(undefined, parseFloat(dcpConfig.job.maxDeployTime));\n } else {\n // If not set programmatically, we keep trying to get an unlocked keystore ... forever.\n let locked = true;\n let safety = 0; // no while loop shall go unguarded\n let ks;\n do {\n ks = null;\n // custom message for the browser modal to denote the purpose of keystore submission\n let msg = `This application is requesting a keystore file to execute ${this.public.description || this.public.name || 'this job'}. Please upload the corresponding keystore file. If you upload a keystore file which has been encrypted with a passphrase, the application will not be able to use it until it prompts for a passphrase and you enter it.`;\n try {\n ks = await wallet.get({ contextId: this.contextId, jobName: this.public.name, msg});\n } catch (e) {\n if (e.code !== ClientModal.CancelErrorCode) throw e;\n };\n if (ks) {\n try {\n await ks.unlock(undefined, parseFloat(dcpConfig.job.maxDeployTime));\n locked = false;\n } catch (e) {\n const expectedCodes = [wallet.unlockFailErrorCode, ClientModal.CancelErrorCode];\n if (!expectedCodes.includes(e.code)) throw e;\n }\n }\n if (safety++ > 1000) throw new Error('EINFINITY: job.exec tried wallet.get more than 1000 times.')\n } while (locked);\n this.setPaymentAccountKeystore(ks)\n }\n\n // We either have a valid keystore + password or we have rejected by this point.\n if (!this.slicePaymentOffer) {\n throw new Error('A payment profile must be assigned before executing the job');\n } else {\n let pd = this[INTERNAL_SYMBOL].payloadDetails;\n pd.feeStructure = this[INTERNAL_SYMBOL].slicePaymentOffer.toFeeStructure(pd.data.length);\n }\n\n if (!this.address) {\n try {\n this.readyStateChange('init');\n await this[DEPLOY_JOB]();\n // localExec jobs are not entered in any compute group.\n if (!this[INTERNAL_SYMBOL].payloadDetails.localExec) {\n // Add this job to its currently-defined compute groups (if any)\n let failedComputeGroups = 0;\n for (const group of this.computeGroups) {\n try\n {\n await computeGroups.addJob(this.address, group);\n this.emit('addComputeGroup', group);\n } catch (error)\n {\n failedComputeGroups++;\n error.message = `Error from compute group with joinKey '${group.joinKey}': ` + error.message;\n this.emit('error', error)\n }\n }\n if (this.computeGroups.length > 0 && failedComputeGroups === this.computeGroups.length)\n {\n this.emit('error', 'Failed to join all specified compute groups, cancelling the job')\n this.cancel('No valid compute groups.');\n }\n\n computeGroups\n .closeServiceConnection()\n .catch((err) =>\n console.error(\n 'Warning: could not close compute groups service connection',\n err,\n ),\n );\n }\n\n this.readyState = 'deployed';\n this.emit('readystatechange', this.readyState);\n this.emit('accepted');\n } catch (error) {\n if (ON_BROWSER) {\n await ClientModal.alert(error, { title: 'Failed to deploy job!' });\n }\n\n throw error;\n }\n } else {\n await this[ADD_LISTENERS]();\n\n this.readyState = 'reconnected';\n this.emit('readystatechange', this.readyState);\n }\n\n this[ON_STATUS](this[INTERNAL_SYMBOL].payloadDetails.status);\n this[INTERNAL_SYMBOL].connected = true;\n\n return new Promise((resolve, reject) => {\n const onComplete = () => resolve(this.results);\n const onCancel = (event) => {\n const reason = `Job was cancelled by ${\n event.reason ? event.reason : 'Cancelled for unknown reason'\n }.`;\n\n /**\n * FIXME(DCP-1150): Remove this since normal cancel event is noisy\n * enough to not need stopped event too.\n */\n if (ON_BROWSER && !this[INTERNAL_SYMBOL].listeningForCancel)\n this[INTERNAL_SYMBOL].cancelAlert(event.reason);\n this.emit('cancel', event);\n reject(new DCPError(event.reason, event.code));\n };\n\n this[INTERNAL_SYMBOL].events.once('stopped', async (stopEvent) => {\n this.emit('stopped', stopEvent.runStatus);\n switch (stopEvent.runStatus) {\n case 'finished':\n if (this.collateResults) {\n let report = await this.getJobInfo();\n\n if(this[INTERNAL_SYMBOL].resultsAvailable.length < report.totalSlices) {\n // fetch results for remain slices\n let fetchedSliceNumbers = this[INTERNAL_SYMBOL].resultsAvailable.reduce((a,e,i) => {\n if(e) a.push(i);\n return a;\n }, []);\n\n let allSliceNumbers = Array.from(Array(report.totalSlices)).map((e,i)=>i+1);\n let remainSliceNumbers = allSliceNumbers.filter( function(e) {\n return !fetchedSliceNumbers.includes(e);\n });\n\n let 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 '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 'cancelled', 'finished', and 'paused'.\n */\n reject(\n new Error(\n `Unknown event \"${stopEvent.runStatus}\" caused the job to be stopped.`,\n ),\n );\n break;\n }\n });\n\n if (!this[INTERNAL_SYMBOL].payloadDetails.running) {\n const runStatus = this[INTERNAL_SYMBOL].payloadDetails.runStatus;\n this[INTERNAL_SYMBOL].events.emit('stopped', { runStatus });\n }\n })\n .finally(() => {\n const handleErr = (e) => {\n console.error('Error while closing job connection:');\n console.error(e);\n }\n\n // Create an async IIFE to not block the promise chain\n (async () => {\n //delay to let last few events to be received\n await new Promise((resolve) => setTimeout(resolve, 1000));\n \n // close all of the connections so that we don't cause node processes to hang.\n await this.eventSubscriber.close().catch(handleErr);\n this.deployConnection.off('close', this.openDeployConn)\n await this.deployConnection.close().catch(handleErr);\n })();\n });\n }\n\n /**\n * job.addListeners(): Private function used to set up event listeners to the scheduler\n * before deploying the job.\n */\n async [ADD_LISTENERS] () {\n // This is important: We need to flush the task queue before adding listeners\n // because we queue pending listeners by listening to the newListener event (in the constructor).\n // If we don't flush here, then the newListener events may fire after this function has run,\n // and the events won't be properly set up.\n await new Promise(resolve => setTimeout(resolve, 0));\n\n // @todo: Listen for an estimated cost, probably emit an \"estimated\" event when it comes in?\n // also @todo: Do the estimation task(s) on the scheduler and send an \"estimated\" event\n\n // Always listen to the stop event. It will resolve the work function promise, so is always needed.\n this.on('stop', (ev) => this[INTERNAL_SYMBOL].events.emit('stopped', ev));\n\n // Connect listeners that were set up before exec\n const evts = Array.from(this[INTERNAL_SYMBOL].subscribedEvents);\n if (evts.includes('result'))\n this.listeningForResults = true;\n this[INTERNAL_SYMBOL].subscribedEvents.clear();\n await this[LISTEN_TO_EVENTS](evts);\n\n // Connect listeners that are set up after exec\n this.on('newListener', (evt) => {\n if (evt === 'newListener') return;\n this[LISTEN_TO_EVENTS]([evt]);\n });\n \n if (this.collateResults && !this.listeningForResults) {\n // automatically add a listener for results\n this.on('result', () => {});\n }\n\n // Connect work event listeners that were set up before exec\n const workEvts = Array.from(this[INTERNAL_SYMBOL].subscribedWorkerEvents);\n this[INTERNAL_SYMBOL].subscribedWorkerEvents.clear();\n for (let evt of workEvts) {\n await this[LISTEN_TO_WORK_EVENTS](evt);\n }\n\n // Connect work event listeners that are set up after exec\n this.work.on('newListener', (evt) => {\n if (evt === 'newListener') return;\n this[LISTEN_TO_WORK_EVENTS](evt);\n });\n }\n\n /**\n * Subscribes to either reliable events or optional events\n * @param {string[]} events \n */\n async [LISTEN_TO_EVENTS](events) {\n\n const reliableEvents = [];\n const optionalEvents = [];\n for (let eventName of events) {\n eventName = eventName.toLowerCase();\n if (this[INTERNAL_SYMBOL].subscribedEvents.has(eventName))\n {\n // already subscribed to this event\n continue;\n }\n else\n {\n this[INTERNAL_SYMBOL].subscribedEvents.add(eventName);\n \n if (this.eventTypes[eventName] && this.eventTypes[eventName].reliable)\n {\n reliableEvents.push(eventName)\n }\n else if (this.eventTypes[eventName] && !this.eventTypes[eventName].reliable)\n {\n optionalEvents.push(eventName)\n }\n else\n {\n // console.debug('606: listening for unexpected/unsupported event:', eventName);\n }\n }\n }\n await this.eventSubscriber.subscribeManyEvents(reliableEvents, optionalEvents, { filter: { job: this.address } })\n }\n\n /**\n * Establishes listeners for worker events when requested by the client\n * @param {string} eventName \n */\n async [LISTEN_TO_WORK_EVENTS](eventName) {\n if (this[INTERNAL_SYMBOL].subscribedWorkerEvents.has(eventName)) {\n // already subscribed to this event\n return;\n }\n else\n {\n this[INTERNAL_SYMBOL].subscribedWorkerEvents.add(eventName);\n this.eventIntercepts.custom = (ev) => this.work.emit(eventName, ev)\n const optionalEvents = ['custom'];\n await this.eventSubscriber.subscribeManyEvents([], optionalEvents, { filter: { job: this.address } });\n }\n }\n\n /**\n * Takes result events as input, stores the result and fires off\n * events on the job handle as required. (result, duplicate-result)\n *\n * @param {object} ev - the event recieved from protocol.listen('/results/0xThisGenAdr')\n */\n async [ON_RESULT] (ev) {\n if (this[INTERNAL_SYMBOL].results === null) {\n // This should never happen - the onResult event should only be established/called\n // in addListeners which should also initialize the internal results array\n throw new Error('Job.onResult was invoked before initializing internal results');\n }\n \n const { result: _result, time } = ev.result;\n let result = await fetchURI(_result);\n try {\n result = kvin.unmarshal(result)\n } catch (error) {\n if (error.message === \"Invalid serialization format\"){\n //Means the slice was returned from a web-worker, which doesn't double-serialize the results like the sa-worker does\n }\n else {\n throw error\n }\n }\n\n if (this[INTERNAL_SYMBOL].results[ev.sliceNumber]) {\n const changed = JSON.stringify(this[INTERNAL_SYMBOL].results[ev.sliceNumber]) !== JSON.stringify(result);\n this.emit('duplicate-result', { sliceNumber: ev.sliceNumber, changed });\n }\n\n this[INTERNAL_SYMBOL].results[ev.sliceNumber] = result;\n this[INTERNAL_SYMBOL].resultsAvailable[ev.sliceNumber] = true;\n this.emit('result', { sliceNumber: ev.sliceNumber, result });\n this.emit('resultsUpdated');\n }\n\n /**\n * Receives status events from the scheduler, updates the local status object\n * and emits a 'status' event\n *\n * @param {object} ev - the status event received from\n * protocol.listen('/status/0xThisGenAdr')\n * @param {boolean} emitStatus - value indicating whether or not the status\n * event should be emitted\n */\n [ON_STATUS]({ runStatus, total, distributed, computed }, emitStatus = true) {\n Object.assign(this[INTERNAL_SYMBOL].status, {\n runStatus,\n total,\n distributed,\n computed,\n });\n\n if (emitStatus) {\n this.emit('status', this.status);\n }\n }\n\n /**\n * Sends a request to the scheduler to deploy the job.\n */\n async [DEPLOY_JOB] () {\n const { payloadDetails } = this[INTERNAL_SYMBOL];\n \n this[SNAPSHOT](); /* .payloadDetails now up to date */\n \n /* Send sideloader bundle to the package server */\n if (DCP_ENV.platform === 'nodejs' && this[INTERNAL_SYMBOL].dependencies.length) {\n let {pkg, unresolved} = await this._publishLocalModules();\n\n payloadDetails.dependencies = unresolved;\n if (pkg)\n payloadDetails.dependencies.push(pkg.name + '/' + sideloaderModuleIdentifier);\n }\n \n this.readyState = 'preauth';\n this.emit('readystatechange', this.readyState);\n\n /* eagerly connect to dependent services for better performance */\n computeGroups.keepAlive()\n \n if (!this.deployConnection || !this.deployConnection.state.is('established')) {\n try {\n await this.deployConnection.connect();\n }\n catch (error) {\n console.error('1046: connection failed', error);\n throw error;\n }\n }\n\n const adhocId = payloadDetails.uuid.slice(payloadDetails.uuid.length - 6, payloadDetails.uuid.length);\n const schedId = this.deployConnection.peerAddress;\n const myId = this.deployConnection.identity;\n const preauthToken = await bankUtil.preAuthorizePayment(schedId, payloadDetails.maxDeployPayment, this.paymentAccountKeystore);\n const { dataRange, dataValues, dataPattern, sliceCount } = marshalInputData(payloadDetails.data);\n\n this.readyState = 'deploying';\n this.emit('readystatechange', this.readyState);\n \n /* Payload format is documented in scheduler-v4/libexec/job-submit/operations/submit.js */\n const submitPayload = {\n owner: myId.address,\n paymentAccount: this.paymentAccountKeystore.address,\n priority: 0, // @nyi\n\n workFunctionURI: payloadDetails.workFunctionURI,\n uuid: payloadDetails.uuid,\n mvScalarSlicePayment: +payloadDetails.feeStructure.marketValue || 0, // @todo: improve feeStructure internals to better reflect v4\n absoluteSlicePayment: +payloadDetails.feeStructure.maxPerRequest || 0,\n requirePath: payloadDetails.requirePath,\n modulePath: payloadDetails.modulePath,\n dependencies: payloadDetails.dependencies,\n requirements: payloadDetails.requirements, /* capex */\n localExec: payloadDetails.localExec,\n\n description: payloadDetails.public.description || 'Discreetly making the world smarter',\n name: payloadDetails.public.name || 'Ad-Hoc Job' + adhocId,\n \n preauthToken, // XXXwg/er @todo: validate this after fleshing out the stub(s)\n\n resultStorageType: payloadDetails.resultStorageType, // @todo: implement other result types\n resultStorageDetails: payloadDetails.resultStorageDetails, // Content depends on resultStorageType\n resultStorageParams: payloadDetails.resultStorageParams, // Post params for off-prem storage\n dataRange,\n marshalizedDataValues: dataValues ? kvin.marshal(dataValues) : dataValues, // serialize input data to avoid passing them as json\n dataPattern,\n sliceCount\n };\n\n /* Determine thee type of the arguments option and set the submit message payload accordingly. */\n if (Array.isArray(payloadDetails.arguments) && payloadDetails.arguments.length === 1 && payloadDetails.arguments[0] instanceof DcpURL) {\n submitPayload.argumentsURI = payloadDetails.arguments[0].href;\n } else if (payloadDetails.arguments instanceof RemoteDataSet) {\n submitPayload.argumentsURI = Array.from(payloadDetails.arguments).join();\n submitPayload.argumentsURI = 'RemoteDataSet:'+ submitPayload.argumentsURI;\n } else if (payloadDetails.arguments) {\n try {\n submitPayload.marshalizedArguments = kvin.marshal(Array.from(payloadDetails.arguments));\n } catch(e) {\n throw new Error(`Could not convert job arguments to Array (${e.message})`);\n }\n }\n\n // XXXpfr Excellent tracing.\n if (debugging('dcp-client')) {\n dumpObject(submitPayload, 'Submit: Job Index: Examine submitPayload', 128);\n }\n\n // Deploy the job!\n const deployed = await this.deployConnection.send('submit', submitPayload, myId);\n\n if (!deployed.success) {\n if(deployed.payload.code === 'ENOTFOUND') {\n throw new DCPError(`Failed to submit job to scheduler. Account: ${submitPayload.paymentAccount} was not found or does not have sufficient balance (${deployed.payload.info.deployCost} DCCs needed to deploy this job)`, deployed.payload);\n } else {\n throw new DCPError('Failed to submit job to scheduler', deployed.payload);\n }\n }\n\n this.address = payloadDetails.address = deployed.payload.job;\n this[INTERNAL_SYMBOL].deployCost = deployed.payload.deployCost;\n \n if (!payloadDetails.status)\n payloadDetails.status = {\n runStatus: null,\n total: 0,\n computed: 0,\n distributed: 0,\n };\n \n payloadDetails.runStatus = payloadDetails.status.runStatus = deployed.payload.status;\n payloadDetails.status.total = deployed.payload.lastSliceNumber;\n payloadDetails.running = true;\n \n this.readyState = 'listeners';\n\n this.emit('readystatechange', this.readyState);\n\n const listenersP = this[ADD_LISTENERS]();\n\n this[INTERNAL_SYMBOL].payloadDetails = {\n ...this[INTERNAL_SYMBOL].payloadDetails,\n ...payloadDetails,\n };\n \n return listenersP;\n }\n\n /**\n * This function is identical to exec, except that the job is executed locally\n * in the client.\n * @async\n * @param {number} cores - the number of local cores in which to execute the job.\n * @param {...any} args - The remaining arguments are identical to the arguments of exec\n * @return {Promise<ResultHandle>} - resolves with the results of the job, rejects on an error\n * @access public\n */\n localExec (cores = 1, ...args) {\n this[INTERNAL_SYMBOL].payloadDetails.localExec = true;\n\n let worker;\n this.on('accepted', () => {\n // Start a worker for this job\n worker = new Worker({\n localExec: true,\n jobAddresses: [this.address],\n paymentAddress: this[INTERNAL_SYMBOL].paymentAccountKeystore.address,\n maxWorkingSandboxes: cores,\n sandboxOptions: {\n ignoreNoProgress: true,\n SandboxConstructor: (DCP_ENV.platform === 'nodejs' &&\n __webpack_require__(/*! ../worker/evaluators */ \"./src/dcp-client/worker/evaluators/index.js\").nodeEvaluatorFactory())\n },\n });\n\n worker.start().catch((e) => {\n console.error(\"Failed to start worker for localExec:\");\n console.error(e.message);\n });\n });\n\n return this.exec(...args).finally(() => {\n if (worker) {\n setTimeout(() => {\n // stop the worker\n worker.stop(true);\n }, 3000);\n }\n });\n }\n\n /**\n * The current job status. Will have undefined values when the handle hasn't had exec called on it yet.\n * @access public\n * @readonly\n * @type {object}\n * @property {number} total Total number of slices in the job\n * @property {number} distributed Number of slices that have been distributed\n * @property {number} computed Number of slices that have completed execution (returned a result)\n * @property {string} runStatus Current runStatus of the job\n */\n get status () {\n // Shallow-copy to prevent modification\n return { ...this[INTERNAL_SYMBOL].status };\n }\n\n get requirePath() {\n return this[INTERNAL_SYMBOL].requirePath;\n }\n\n /**\n * This function specifies a module dependency (when the argument is a string)\n * or a list of dependencies (when the argument is an array) of the work\n * function. This function can be invoked multiple times before deployment.\n * @param {string | string[]} modulePaths - A string or array describing one\n * or more dependencies of the job.\n * @access public\n */\n requires(modulePaths) {\n if (\n typeof modulePaths !== 'string' &&\n (!Array.isArray(modulePaths) ||\n modulePaths.some((modulePath) => typeof modulePath !== 'string'))\n ) {\n throw new TypeError(\n 'The argument to dependencies is not a string or an array of strings',\n );\n } else if (modulePaths.length === 0) {\n throw new RangeError(\n 'The argument to dependencies cannot be an empty string or array',\n );\n } else if (\n Array.isArray(modulePaths) &&\n modulePaths.some((modulePath) => modulePath.length === 0)\n ) {\n throw new RangeError(\n 'The argument to dependencies cannot be an array containing an empty string',\n );\n }\n\n if (!Array.isArray(modulePaths)) {\n modulePaths = [modulePaths];\n }\n\n for (const modulePath of modulePaths) {\n if (modulePath[0] !== '.' && modulePath.indexOf('/') !== -1) {\n const modulePrefixRegEx = /^(.*)\\/.*?$/;\n const [, modulePrefix] = modulePath.match(modulePrefixRegEx);\n if (\n modulePrefix &&\n this[INTERNAL_SYMBOL].requirePath.indexOf(modulePrefix) === -1\n ) {\n this[INTERNAL_SYMBOL].requirePath.push(modulePrefix);\n }\n }\n\n this[INTERNAL_SYMBOL].dependencies.push(modulePath);\n }\n }\n\n get slicePaymentOffer () {\n return this[INTERNAL_SYMBOL].slicePaymentOffer;\n }\n\n /**\n * The keystore that will be used to pay for the job. Can be set with {@link Job.setPaymentAccountKeystore} or by providing a keystore to {@link Job.exec}.\n * @readonly\n * @access public\n * @type {module:dcp/wallet.AuthKeystore}\n */\n get paymentAccountKeystore () {\n return this[INTERNAL_SYMBOL].paymentAccountKeystore;\n }\n\n /** Set the account upon which funds will be drawn to pay for the job.\n * @param {module:dcp/wallet.AuthKeystore} keystore A keystore that representa a bank account.\n * @access public\n */\n setPaymentAccountKeystore (keystore) {\n if (this.address) {\n if (!keystore.address.eq(this[INTERNAL_SYMBOL].payloadDetails.owner)) {\n let message = \"Cannot change payment account after job has been deployed\";\n this.emit('EPERM', message);\n throw new Error(`EPERM: ${message}`);\n }\n }\n \n if (!(keystore instanceof wallet.Keystore)) {\n let e = new Error('Not an instance of Keystore: ' + keystore.toString())\n console.log(`Not an instance of Keystore: ${keystore}`)\n throw e\n }\n this[INTERNAL_SYMBOL].paymentAccountKeystore = keystore;\n }\n\n /** Set the slice payment offer. This is equivalent to the first argument to exec.\n * @param {number} slicePaymentOffer - The number of DCC the user is willing to pay to compute one slice of this job\n */\n setSlicePaymentOffer (slicePaymentOffer) {\n this[INTERNAL_SYMBOL].slicePaymentOffer = new SlicePaymentOffer(slicePaymentOffer);\n }\n\n /**\n * @param {URL} locationUrl - A URL object \n * @param {object} postParams - An object with any parameters that a user would like to be passed to a \n * remote result location. This object is capable of carry API keys for S3, \n * DropBox, etc. These parameters are passed as parameters in an \n * application/x-www-form-urlencoded request.\n */\n setResultStorage(locationUrl, postParams) {\n if (locationUrl instanceof URL || locationUrl instanceof DcpURL) {\n this[INTERNAL_SYMBOL].resultStorageDetails = locationUrl;\n } else {\n const e = new Error('Not an instance of a DCP URL: ' + locationUrl);\n console.log('Not an instance of a DCP URL ' + locationUrl);\n throw e;\n }\n\n // resultStorageParams contains any post params required for off-prem storage\n if (typeof postParams !== 'undefined' && typeof postParams === 'object' ) {\n this[INTERNAL_SYMBOL].resultStorageParams = postParams;\n } else {\n const e = new Error('Not an instance of a object: ' + postParams);\n console.log('Not an instance of an object ' + postParams);\n throw e;\n } \n\n // Some type of object here\n this[INTERNAL_SYMBOL].resultStorageType = 'pattern';\n } \n}\n\n/**\n * @typedef {object} MarshaledInputData\n * @property {any[]} [dataValues]\n * @property {string} [dataPattern]\n * @property {RangeObject} [dataRange]\n * @property {number} [sliceCount]\n */\n\n/**\n * Depending on the shape of the job's data, resolve it into a RangeObject, a\n * Pattern, or a values array, and return it in the appropriate property.\n *\n * @param {any} data Job's input data\n * @return {MarshaledInputData} An object with one of the following properties set:\n * - dataValues: job input is an array of arbitrary values \n * - dataPattern: job input is a URI pattern \n * - dataRange: job input is a RangeObject (and/or friends)\n */\nfunction marshalInputData(data) {\n if (!(data instanceof Object\n || data instanceof SuperRangeObject)) {\n throw new TypeError(`Invalid job data type: ${typeof data}`);\n }\n\n /**\n * @type MarshaledInputData\n */\n const marshalledInputData = {};\n\n // TODO(wesgarland): Make this more robust.\n if (data instanceof SuperRangeObject ||\n (data.hasOwnProperty('ranges') && data.ranges instanceof MultiRangeObject) ||\n (data.hasOwnProperty('start') && data.hasOwnProperty('end'))) {\n marshalledInputData.dataRange = data;\n } else if (Array.isArray(data)) {\n marshalledInputData.dataValues = data;\n } else if (data instanceof URL || data instanceof DcpURL) {\n marshalledInputData.dataPattern = String(data);\n } else if(data instanceof RemoteDataSet) {\n marshalledInputData.dataValues = data.map(e => new URL(e));\n } else if(data instanceof RemoteDataPattern) {\n marshalledInputData.dataPattern = data['pattern'];\n marshalledInputData.sliceCount = data['sliceCount'];\n }\n\n log('marshalledInputData:', marshalledInputData);\n return marshalledInputData;\n}\n\nObject.assign(exports, {\n Job,\n SlicePaymentOffer,\n ResultHandle,\n});\n\n\n//# sourceURL=webpack:///./src/dcp-client/job/index.js?");
5114
5103
 
5115
5104
  /***/ }),
5116
5105
 
@@ -5122,7 +5111,7 @@ eval("/**\n * @file job/index.js\n * @author Eddie Roosenmaallen, ed
5122
5111
  /***/ (function(module, exports, __webpack_require__) {
5123
5112
 
5124
5113
  "use strict";
5125
- eval("/**\n * @file node-modules.js Node-specific support for sending jobs to the scheduler\n * which use modules supplied by the local filesystem.\n * @author Wes Garland, wes@kingsds.network\n * @date May 2020\n */\n\nconst { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\nconst utils = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\nconst process = requireNative('process');\nconst path = requireNative('path');\nconst os = requireNative('os');\nconst fs = requireNative('fs');\nconst pathPrivacyMode = true; /** @todo XXX tie into job, false for localExec(?) */\nconst debugging = __webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope('job-modules');\n\nif (!debugging() && exports.debug) {\n debugging = function nodeModules$$debugging() { return true };\n}\n\n/** \n * Fix windows backslashes, which are a problem when re-interpreted by functions like fs.open() \n * (because they are interpreted as escapes). This \"fix\" changes them from backslashes to forward\n * slashes, which node (and hench webpack) errorneously interpret as directory separators on win32.\n * \n * Doing this fix every time we get a path from the OS (eg via path or require.resolve) means we \n * can mostly treat windows paths, unix paths, and commonjs paths the same way everywhere, with the\n * notable except of absolute paths like c:/path/to/module.js - these must be re-written when used\n * as part of a canonical module identifier (into /mnt/c/path/to/module.js), and must be detected\n * via path.isAbsolute() rather than looking at the first character of the path.\n */\nfunction fixSlashes(diskPath) {\n diskPath = os.platform() === 'win32' ? diskPath.replace(/[\\\\]/g, '/') : diskPath;\n return diskPath;\n}\n\n/** Create a module bundle in the form of a BravoJS module which, when evaluated, injects a module bunch of\n * modules into the running BravoJS environment. The module bundles are currently created for NodeJS via\n * webpack, but that is an implementation detail.\n *\n * @param ${requireList} {string | Array} module identifier or list of module identifiers\n * @param buildMode {string | undefined} 'debug' for source maps or 'release' for minified\n * @param outerRequrire {string | undefined} require function to use to resolve module identifiers.\n * If undefined, resolve relative to program module.\n *\n * @todo This needs to be changed to use async calls for the spawned process (maintaining stdio mappings!) \n * and the file copying operations, so that we can deploy the job at the same time as the I/O heavy\n * stuff is happening on the local box. /wg may 2020\n */\nexports.createModuleBundle = function job$$createModuleBundle(requireList, buildMode, outerRequire) {\n const webpack = requireNative('webpack');\n const child_process = requireNative('child_process');\n \n var configuration;\n var webpackOutput, webpackInput, webpackConfig, finalOutput;\n var unresolved = []; /* list of module identifiers which could not be resolved (hopefully are deployed on network) */\n var bundlePathMap = {}; /* table mapping canonical id to real path. Usually similar, not always (thanks npm) */\n var resolveMap = {}; /* table mapping module identifier to canonical id */\n var searchPaths = {}; /* table (that becomes a list) enumerating searchable directories discovered during resolution */\n var mainModuleDir, modulePaths;\n var child;\n var args = [];\n var prologue, epilogue, md5Stream;\n\n if (!buildMode)\n buildMode = 'debug';\n if (!outerRequire)\n outerRequire = requireNative('module').createRequireFromPath(process.argv[1]);\n\n mainModuleDir = canonicalize(fixSlashes(path.dirname(outerRequire.main.filename)), outerRequire, null, []);\n modulePaths = outerRequire.main.paths;\n\n for (let moduleIdentifier of requireList) {\n let canonicalPath = canonicalize(moduleIdentifier, outerRequire, mainModuleDir, modulePaths);\n if (!canonicalPath) {\n unresolved.push(moduleIdentifier);\n continue;\n }\n bundlePathMap[canonicalPath] = moduleIdentifier;\n resolveMap[moduleIdentifier] = canonicalPath;\n if (canonicalPath.startsWith('/localhost/program/node_modules/') || canonicalPath.startsWith('/localhost/search/node_modules/')) {\n let npmDir = canonicalPath.split('/').slice(0,5).join('/');\n searchPaths[npmDir] = true;\n } else if (canonicalPath.startsWith('/localhost/search/')) {\n let npmDir = canonicalPath.split('/').slice(0,4).join('/');\n searchPaths[npmDir] = true;\n }\n }\n if (Object.keys(bundlePathMap).length === 0) {\n return { unresolved };\n }\n \n /* Create the module bundle, exporting the modules that will be packed into it \n * and annotating the effects of search path expansion package.json magic.\n */\n try {\n webpackOutput = utils.createTempFile(`dcp-${process.pid}-module-bundle-XXXXXXXX`, 'js').closeSync();\n webpackInput = utils.createTempFile(`dcp-${process.pid}-module-index-XXXXXXXX`, 'js');\n webpackConfig = utils.createTempFile(`dcp-${process.pid}-module-config-XXXXXXXX`, 'js');\n finalOutput = utils.createTempFile(`dcp-${process.pid}-final-bundle-XXXXXXXX`, 'js');\n\n searchPaths = Object.keys(searchPaths);\n webpackInput.writeSync('Object.assign(exports, {\\n');\n for (let moduleId in bundlePathMap) {\n let diskPath = fixSlashes(outerRequire.resolve(bundlePathMap[moduleId]));\n webpackInput.writeSync(` \"${moduleId}\": require(\"${diskPath}\"),\\n`);\n }\n \n webpackInput.writeSync('});\\n');\n webpackInput.writeSync(`defineSearchPaths(${JSON.stringify(searchPaths)});\\n`);\n webpackInput.writeSync(`defineResolveMap(${JSON.stringify(resolveMap)});\\n`);\n /* Run webpack in a separate process to work around issues with require()ing it\n * from a local git repo via npm which is not this git repo. The output of the\n * webpack is the module bundle, in amd define() transport format.\n */\n configuration = {\n mode: buildMode === 'debug' ? 'development' : 'release',\n optimization: { minimize: buildMode === 'debug' },\n entry: webpackInput.filename,\n output: {\n path: path.dirname(webpackOutput.filename),\n filename: path.basename(webpackOutput.filename),\n libraryTarget: 'amd',\n library: 'dcp-localhost',\n },\n }\n\n switch(parseInt(webpack.version, 10))\n {\n case 0: case 1: case 2: case 3:\n throw new Error('Webpack ' + webpack.version + ' is not supported');\n case 4:\n {\n console.warn('Warning: webpack 4 is deprecated - please upgrade!');\n configuration.output.ecmaVersion = 6;\n configuration.output.pathinfo = buildMode === 'debug';\n\n // Remove fs module from generated bundle\n configuration.node = { fs: 'empty' };\n\n if (!process.env.DCP_DEBUG_WEBPACK)\n {\n args.push('--log-level=warn');\n }\n break;\n }\n default:\n console.warn('Warning: webpack ' + webpack.version + ' not officially supported');\n /* fallthrough */\n case 5:\n {\n configuration.output.pathinfo = buildMode === 'debug';\n configuration.target = [ 'es6' ];\n\n // Need to set a default script chunk format based on the target.\n configuration.output.chunkFormat = 'commonjs';\n\n configuration.resolve = {\n fallback: {\n // Remove fs module from generated bundle\n fs: false,\n }\n };\n break;\n }\n }\n\n args[0] = process.execPath;\n args[1] = fixSlashes(path.resolve(path.dirname(requireNative.resolve('webpack')), '..', 'bin', 'webpack.js'));\n\n webpackConfig.writeSync('module.exports=' + JSON.stringify(configuration));\n args.push(`--config=${webpackConfig.filename}`);\n\n if (dcpConfig.job && dcpConfig.job.modules && dcpConfig.job.modules.webpackOptions)\n args = args.concat(utils.tokenize(dcpConfig.job.modules.webpackOptions));\n if (process.env.DCP_NODE_MODULES_WEBPACK_OPTIONS)\n args = args.concat(utils.tokenize(process.env.DCP_NODE_MODULES_WEBPACK_OPTIONS));\n\n debugging() && console.log('Creating job.requires(dependencies) bundle:', args);\n child = child_process.spawnSync(args[0], args.slice(1), {\n shell: false,\n windowsHide: true,\n stdio: 'inherit',\n });\n\n if (child.status !== 0)\n throw new Error(`Child process returned exit code ${child.status}`);\n \n /* Add a wrapper function around the webpack output which turns it into a valid\n * CommonJS Modules/2.0-draft8 (eg BravoJS) module that does not pollute scope.\n * Calculate the while we're at it to avoid double-reading large files.\n */\n ({prologue, epilogue} = sideloadGenerator());\n md5Stream = __webpack_require__(/*! create-hash */ \"./node_modules/create-hash/browser.js\")('md5');\n finalOutput.writeSync(prologue);\n md5Stream.write(prologue);\n utils.catFile(webpackOutput, finalOutput, md5Stream);\n finalOutput.writeSync(epilogue);\n md5Stream.write(epilogue);\n md5Stream.end();\n } finally {\n webpackInput.closeSync();\n webpackConfig.closeSync();\n finalOutput.closeSync();\n\n webpackInput.remove();\n webpackConfig.remove();\n webpackOutput.remove();\n }\n\n return {\n tempFile: finalOutput,\n hash: md5Stream.read(),\n unresolved: unresolved,\n }\n}\n\n/** \n * Inject the modules in a module bundle into the current BravoJS environment's\n * internal module memo, after invoking module.declare() so that we can masquerade \n * as a simple module dependency.\n *\n * @note This function exists in source form so that it isn't annoying to edit. \n * It gets used by being converted to string and parsed by sideloadGenerator().\n * The line containing the define(\"webpack...\") string must not be changed, \n * as it is a parse token.\n *\n * Additionally, all newlines will be eliminated to preserve the webpack source\n * map, so no C++-style comments should be used, and semicolons are mandatory.\n * Whitespace is also compacted, even inside string literals.\n */\nconst sideloader = `function sideloader() {\n module.declare(function(){});\n (function IIFE() { /* inject webpacked module group into BravoJS' module memo */\n /* Add program module paths discovered during bundling to global require path */\n function defineSearchPaths(searchPaths) {\n for (path of searchPaths)\n bravojs.paths.push(path);\n }\n /* Add name mapping for requires args to cover cases where resolution happens in package.json */\n function defineResolveMap(pathMap) {\n if (!bravojs.resolveMap)\n bravojs.resolveMap = {};\n bravojs.resolveMap = Object.assign(bravojs.resolveMap, pathMap);\n if (!bravojs.globalResolveHook) {\n bravojs.globalResolveHook = function dcp$$define$resolve(moduleIdentifier) {\n if (bravojs.resolveMap.hasOwnProperty(moduleIdentifier))\n return bravojs.resolveMap[moduleIdentifier];\n else\n return moduleIdentifier;\n }\n }\n }\n function define(amdModuleLabel, amdModuleDeps, amdModuleFactory) {\n var moduleGroup = amdModuleFactory();\n\n for (let moduleIdentifier in moduleGroup) {\n if (!moduleGroup.hasOwnProperty(moduleIdentifier))\n continue;\n bravojs.requireMemo[moduleIdentifier] = moduleGroup[moduleIdentifier];\n if (bravojs.debug)\n console.log(\\`Memoized module \\${moduleIdentifier}: \\${Object.keys(moduleGroup[moduleIdentifier])}\\`);\n }\n if (bravojs.mainModule !== '/localhost/program') {\n bravojs.mainModuleDir = '/localhost/program';\n bravojs.global.require = bravojs.requireFactory(bravojs.mainModuleDir);\n }\n }\n define(\"webpack amd output here\", [], factoryFunction);\n })();\n}`;\n\n/** Generate the prologue and epilogue for a webpack in amd library mode which, when \n * all three parts are joined, will sideload the webpack's exports into BravoJS'\n * running require memo, exposing them as require() modules.\n */\nfunction sideloadGenerator() {\n var slInner = sideloader.toString().replace(/^[^{]*{/,'').replace(/} *$/,'');\n var offset_define = slInner.indexOf('define(\"webpack amd output here\"');\n var offset_epilogue = slInner.slice(offset_define).indexOf('\\n');\n var prologue, epilogue;\n \n if (offset_define === -1 || offset_epilogue === -1) {\n throw new Error(`sideload function missing parse token(s)`);\n }\n\n prologue = slInner.slice(0, offset_define) .replace(/[ \\n\\r\\t]+/g, ' ').replace(/(^ | $)/g,'');\n epilogue = slInner.slice(offset_define + offset_epilogue).replace(/[ \\n\\r\\t]+/g, ' ').replace(/(^ | $)/g,'');\n\n return {prologue, epilogue};\n}\n\n/** Return the canonical module id for a given module. All of the modules begin\n * begin in the localhost directory off the conceptual module system root, as these\n * are \"local\" -- not deployed -- modules. Otherwise, they mostly represent the\n * structure on disk, although this is explicitly a path within the conceptual module\n * system space and not actually any physical filesystem.\n * \n * In \"release\" mode, we further modify the canonical path to preserve privacy\n * wrt the developer's disk layout: All search-path directories are \"collapsed\" \n * onto a single directory, and the program-module directory is anonymized. In\n * that case, the program module's \"directory\" is /localhost/program/ and the\n * search path is /localhost/search/.\n *\n * @param {string} moduleIdentifier a module identifier (argument to require()) or absolute path to module\n * @param {function} outerRequire the require() function used by the node code (not the work function)\n * @param {string} mainModuleDir the directory of the main (program) module; used to calculate safety\n * of relative paths when in path-privacy mode. Passing excatly null\n * disables this path-privacy mode.\n * @param {Array} modulePaths List of paths to search (like require.paths in CJS)\n */\nfunction canonicalize(moduleIdentifier, outerRequire, mainModuleDir, modulePaths) {\n var filename, fno;\n var canonical = '';\n var doPathPrivacy = pathPrivacyMode && mainModuleDir !== null;\n \n try {\n filename = (path.isAbsolute(moduleIdentifier) ? path : outerRequire).resolve(moduleIdentifier);\n fixSlashes(filename);\n } catch(e) {\n if (moduleIdentifier.startsWith('./') ||\n moduleIdentifier.startsWith('../') ||\n moduleIdentifier.startsWith('/') ||\n path.isAbsolute(moduleIdentifier)) {\n throw new Error(`Cannot locate module '${moduleIdentifier}'`);\n }\n debugging() && console.warn(`Cannot locate module ${moduleIdentifier}; assuming on deployed search path`);\n return null;\n }\n\n fno = path.parse(filename);\n if (os.platform() === 'win32' && fno.root.match(/[A-Za-z]:/)) {\n /* put drive letters into a virtual mount point */\n let newDir = '/mnt/' + fno.root[0].toLowerCase();\n if (fno.dir != fno.root)\n newDir += '/' + fno.dir.slice(fno.root.length);\n fno.root = '/';\n fno.dir = newDir;\n }\n /* root is always /\n * dir always starts with a / unless its empty and does not end in a /\n * base never starts with a / and does not end in a /\n */ \n canonical = fno.root;\n if (fno.dir)\n canonical += fno.dir.slice(fno.root.length);\n canonical += '/' + fno.name;\n\n if (doPathPrivacy) {\n let mmRE = new RegExp(`^${mainModuleDir}(/|$)`);\n if (canonical.match(mmRE)) {\n canonical = '/program' + canonical.slice(mainModuleDir.length);\n } else {\n if (moduleIdentifier.startsWith('./') || moduleIdentifier.startsWith('../')) {\n throw new Error(`Relative module identifiers above the program module's directory are not supported in path privacy mode (${canonical})`);\n }\n canonical = '/search' + moduleIdentifier;\n }\n }\n\n if (doPathPrivacy)\n return '/localhost' + canonical;\n else\n return canonical;\n}\n\n\n//# sourceURL=webpack:///./src/dcp-client/job/node-modules.js?");
5114
+ eval("/**\n * @file node-modules.js Node-specific support for sending jobs to the scheduler\n * which use modules supplied by the local filesystem.\n * @author Wes Garland, wes@kingsds.network\n * @date May 2020\n */\n\nconst { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\nconst utils = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\nconst process = requireNative('process');\nconst path = requireNative('path');\nconst os = requireNative('os');\nconst fs = requireNative('fs');\nconst pathPrivacyMode = true; /** @todo XXX tie into job, false for localExec(?) */\nconst debugging = __webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope('job-modules');\n\nif (!debugging() && exports.debug) {\n debugging = function nodeModules$$debugging() { return true };\n}\n\n/** \n * Fix windows backslashes, which are a problem when re-interpreted by functions like fs.open() \n * (because they are interpreted as escapes). This \"fix\" changes them from backslashes to forward\n * slashes, which node (and hench webpack) errorneously interpret as directory separators on win32.\n * \n * Doing this fix every time we get a path from the OS (eg via path or require.resolve) means we \n * can mostly treat windows paths, unix paths, and commonjs paths the same way everywhere, with the\n * notable except of absolute paths like c:/path/to/module.js - these must be re-written when used\n * as part of a canonical module identifier (into /mnt/c/path/to/module.js), and must be detected\n * via path.isAbsolute() rather than looking at the first character of the path.\n */\nfunction fixSlashes(diskPath) {\n diskPath = os.platform() === 'win32' ? diskPath.replace(/[\\\\]/g, '/') : diskPath;\n return diskPath;\n}\n\n/** Create a module bundle in the form of a BravoJS module which, when evaluated, injects a module bunch of\n * modules into the running BravoJS environment. The module bundles are currently created for NodeJS via\n * webpack, but that is an implementation detail.\n *\n * @param ${requireList} {string | Array} module identifier or list of module identifiers\n * @param buildMode {string | undefined} 'debug' for source maps or 'release' for minified\n * @param outerRequrire {string | undefined} require function to use to resolve module identifiers.\n * If undefined, resolve relative to program module.\n *\n * @todo This needs to be changed to use async calls for the spawned process (maintaining stdio mappings!) \n * and the file copying operations, so that we can deploy the job at the same time as the I/O heavy\n * stuff is happening on the local box. /wg may 2020\n */\nexports.createModuleBundle = function job$$createModuleBundle(requireList, buildMode, outerRequire) {\n const webpack = requireNative('webpack');\n const child_process = requireNative('child_process');\n \n var configuration;\n var webpackOutput, webpackInput, webpackConfig, finalOutput;\n var unresolved = []; /* list of module identifiers which could not be resolved (hopefully are deployed on network) */\n var bundlePathMap = {}; /* table mapping canonical id to real path. Usually similar, not always (thanks npm) */\n var resolveMap = {}; /* table mapping module identifier to canonical id */\n var searchPaths = {}; /* table (that becomes a list) enumerating searchable directories discovered during resolution */\n var mainModuleDir, modulePaths;\n var child;\n var args = [];\n var prologue, epilogue, md5Stream;\n\n if (!buildMode)\n buildMode = 'debug';\n if (!outerRequire)\n outerRequire = requireNative('module').createRequireFromPath(process.argv[1]);\n\n mainModuleDir = canonicalize(fixSlashes(path.dirname(outerRequire.main.filename)), outerRequire, null, []);\n modulePaths = outerRequire.main.paths;\n\n for (let moduleIdentifier of requireList) {\n let canonicalPath = canonicalize(moduleIdentifier, outerRequire, mainModuleDir, modulePaths);\n if (!canonicalPath) {\n unresolved.push(moduleIdentifier);\n continue;\n }\n bundlePathMap[canonicalPath] = moduleIdentifier;\n resolveMap[moduleIdentifier] = canonicalPath;\n if (canonicalPath.startsWith('/localhost/program/node_modules/') || canonicalPath.startsWith('/localhost/search/node_modules/')) {\n let npmDir = canonicalPath.split('/').slice(0,5).join('/');\n searchPaths[npmDir] = true;\n } else if (canonicalPath.startsWith('/localhost/search/')) {\n let npmDir = canonicalPath.split('/').slice(0,4).join('/');\n searchPaths[npmDir] = true;\n }\n }\n if (Object.keys(bundlePathMap).length === 0) {\n return { unresolved };\n }\n \n /* Create the module bundle, exporting the modules that will be packed into it \n * and annotating the effects of search path expansion package.json magic.\n */\n try {\n webpackOutput = utils.createTempFile(`dcp-${process.pid}-module-bundle-XXXXXXXX`, 'js').closeSync();\n webpackInput = utils.createTempFile(`dcp-${process.pid}-module-index-XXXXXXXX`, 'js');\n webpackConfig = utils.createTempFile(`dcp-${process.pid}-module-config-XXXXXXXX`, 'js');\n finalOutput = utils.createTempFile(`dcp-${process.pid}-final-bundle-XXXXXXXX`, 'js');\n\n searchPaths = Object.keys(searchPaths);\n webpackInput.writeSync('Object.assign(exports, {\\n');\n for (let moduleId in bundlePathMap) {\n let diskPath = fixSlashes(outerRequire.resolve(bundlePathMap[moduleId]));\n webpackInput.writeSync(` \"${moduleId}\": require(\"${diskPath}\"),\\n`);\n }\n \n webpackInput.writeSync('});\\n');\n webpackInput.writeSync(`defineSearchPaths(${JSON.stringify(searchPaths)});\\n`);\n webpackInput.writeSync(`defineResolveMap(${JSON.stringify(resolveMap)});\\n`);\n /* Run webpack in a separate process to work around issues with require()ing it\n * from a local git repo via npm which is not this git repo. The output of the\n * webpack is the module bundle, in amd define() transport format.\n */\n configuration = {\n mode: buildMode === 'debug' ? 'development' : 'release',\n optimization: { minimize: buildMode === 'debug' },\n entry: webpackInput.filename,\n output: {\n path: path.dirname(webpackOutput.filename),\n filename: path.basename(webpackOutput.filename),\n libraryTarget: 'amd',\n library: 'dcp-localhost',\n },\n }\n\n switch(parseInt(webpack.version, 10))\n {\n case 0: case 1: case 2: case 3:\n throw new Error('Webpack ' + webpack.version + ' is not supported');\n case 4:\n {\n console.warn('Warning: webpack 4 is deprecated - please upgrade!');\n // configuration.output.ecmaVersion = 6;\n configuration.output.pathinfo = buildMode === 'debug';\n\n // Remove fs module from generated bundle\n configuration.node = { fs: 'empty' };\n\n if (!process.env.DCP_DEBUG_WEBPACK)\n {\n args.push('--log-level=warn');\n }\n break;\n }\n default:\n console.warn('Warning: webpack ' + webpack.version + ' not officially supported');\n /* fallthrough */\n case 5:\n {\n configuration.output.pathinfo = buildMode === 'debug';\n configuration.target = [ 'es6' ];\n\n // Need to set a default script chunk format based on the target.\n configuration.output.chunkFormat = 'commonjs';\n\n configuration.resolve = {\n fallback: {\n // Remove fs module from generated bundle\n fs: false,\n }\n };\n break;\n }\n }\n\n args[0] = process.execPath;\n args[1] = fixSlashes(path.resolve(path.dirname(requireNative.resolve('webpack')), '..', 'bin', 'webpack.js'));\n\n webpackConfig.writeSync('module.exports=' + JSON.stringify(configuration));\n args.push(`--config=${webpackConfig.filename}`);\n\n if (dcpConfig.job && dcpConfig.job.modules && dcpConfig.job.modules.webpackOptions)\n args = args.concat(utils.tokenize(dcpConfig.job.modules.webpackOptions));\n if (process.env.DCP_NODE_MODULES_WEBPACK_OPTIONS)\n args = args.concat(utils.tokenize(process.env.DCP_NODE_MODULES_WEBPACK_OPTIONS));\n\n debugging() && console.log('Creating job.requires(dependencies) bundle:', args);\n child = child_process.spawnSync(args[0], args.slice(1), {\n shell: false,\n windowsHide: true,\n stdio: 'inherit',\n });\n\n if (child.status !== 0)\n throw new Error(`Child process returned exit code ${child.status}`);\n \n /* Add a wrapper function around the webpack output which turns it into a valid\n * CommonJS Modules/2.0-draft8 (eg BravoJS) module that does not pollute scope.\n * Calculate the while we're at it to avoid double-reading large files.\n */\n ({prologue, epilogue} = sideloadGenerator());\n md5Stream = __webpack_require__(/*! create-hash */ \"./node_modules/create-hash/browser.js\")('md5');\n finalOutput.writeSync(prologue);\n md5Stream.write(prologue);\n utils.catFile(webpackOutput, finalOutput, md5Stream);\n finalOutput.writeSync(epilogue);\n md5Stream.write(epilogue);\n md5Stream.end();\n } finally {\n webpackInput.closeSync();\n webpackConfig.closeSync();\n finalOutput.closeSync();\n\n webpackInput.remove();\n webpackConfig.remove();\n webpackOutput.remove();\n }\n\n return {\n tempFile: finalOutput,\n hash: md5Stream.read(),\n unresolved: unresolved,\n }\n}\n\n/** \n * Inject the modules in a module bundle into the current BravoJS environment's\n * internal module memo, after invoking module.declare() so that we can masquerade \n * as a simple module dependency.\n *\n * @note This function exists in source form so that it isn't annoying to edit. \n * It gets used by being converted to string and parsed by sideloadGenerator().\n * The line containing the define(\"webpack...\") string must not be changed, \n * as it is a parse token.\n *\n * Additionally, all newlines will be eliminated to preserve the webpack source\n * map, so no C++-style comments should be used, and semicolons are mandatory.\n * Whitespace is also compacted, even inside string literals.\n */\nconst sideloader = `function sideloader() {\n module.declare(function(){});\n (function IIFE() { /* inject webpacked module group into BravoJS' module memo */\n /* Add program module paths discovered during bundling to global require path */\n function defineSearchPaths(searchPaths) {\n for (path of searchPaths)\n bravojs.paths.push(path);\n }\n /* Add name mapping for requires args to cover cases where resolution happens in package.json */\n function defineResolveMap(pathMap) {\n if (!bravojs.resolveMap)\n bravojs.resolveMap = {};\n bravojs.resolveMap = Object.assign(bravojs.resolveMap, pathMap);\n if (!bravojs.globalResolveHook) {\n bravojs.globalResolveHook = function dcp$$define$resolve(moduleIdentifier) {\n if (bravojs.resolveMap.hasOwnProperty(moduleIdentifier))\n return bravojs.resolveMap[moduleIdentifier];\n else\n return moduleIdentifier;\n }\n }\n }\n function define(amdModuleLabel, amdModuleDeps, amdModuleFactory) {\n var moduleGroup = amdModuleFactory();\n\n for (let moduleIdentifier in moduleGroup) {\n if (!moduleGroup.hasOwnProperty(moduleIdentifier))\n continue;\n bravojs.requireMemo[moduleIdentifier] = moduleGroup[moduleIdentifier];\n if (bravojs.debug)\n console.log(\\`Memoized module \\${moduleIdentifier}: \\${Object.keys(moduleGroup[moduleIdentifier])}\\`);\n }\n if (bravojs.mainModule !== '/localhost/program') {\n bravojs.mainModuleDir = '/localhost/program';\n bravojs.global.require = bravojs.requireFactory(bravojs.mainModuleDir);\n }\n }\n define(\"webpack amd output here\", [], factoryFunction);\n })();\n}`;\n\n/** Generate the prologue and epilogue for a webpack in amd library mode which, when \n * all three parts are joined, will sideload the webpack's exports into BravoJS'\n * running require memo, exposing them as require() modules.\n */\nfunction sideloadGenerator() {\n var slInner = sideloader.toString().replace(/^[^{]*{/,'').replace(/} *$/,'');\n var offset_define = slInner.indexOf('define(\"webpack amd output here\"');\n var offset_epilogue = slInner.slice(offset_define).indexOf('\\n');\n var prologue, epilogue;\n \n if (offset_define === -1 || offset_epilogue === -1) {\n throw new Error(`sideload function missing parse token(s)`);\n }\n\n prologue = slInner.slice(0, offset_define) .replace(/[ \\n\\r\\t]+/g, ' ').replace(/(^ | $)/g,'');\n epilogue = slInner.slice(offset_define + offset_epilogue).replace(/[ \\n\\r\\t]+/g, ' ').replace(/(^ | $)/g,'');\n\n return {prologue, epilogue};\n}\n\n/** Return the canonical module id for a given module. All of the modules begin\n * begin in the localhost directory off the conceptual module system root, as these\n * are \"local\" -- not deployed -- modules. Otherwise, they mostly represent the\n * structure on disk, although this is explicitly a path within the conceptual module\n * system space and not actually any physical filesystem.\n * \n * In \"release\" mode, we further modify the canonical path to preserve privacy\n * wrt the developer's disk layout: All search-path directories are \"collapsed\" \n * onto a single directory, and the program-module directory is anonymized. In\n * that case, the program module's \"directory\" is /localhost/program/ and the\n * search path is /localhost/search/.\n *\n * @param {string} moduleIdentifier a module identifier (argument to require()) or absolute path to module\n * @param {function} outerRequire the require() function used by the node code (not the work function)\n * @param {string} mainModuleDir the directory of the main (program) module; used to calculate safety\n * of relative paths when in path-privacy mode. Passing excatly null\n * disables this path-privacy mode.\n * @param {Array} modulePaths List of paths to search (like require.paths in CJS)\n */\nfunction canonicalize(moduleIdentifier, outerRequire, mainModuleDir, modulePaths) {\n var filename, fno;\n var canonical = '';\n var doPathPrivacy = pathPrivacyMode && mainModuleDir !== null;\n \n try {\n filename = (path.isAbsolute(moduleIdentifier) ? path : outerRequire).resolve(moduleIdentifier);\n fixSlashes(filename);\n } catch(e) {\n if (moduleIdentifier.startsWith('./') ||\n moduleIdentifier.startsWith('../') ||\n moduleIdentifier.startsWith('/') ||\n path.isAbsolute(moduleIdentifier)) {\n throw new Error(`Cannot locate module '${moduleIdentifier}'`);\n }\n debugging() && console.warn(`Cannot locate module ${moduleIdentifier}; assuming on deployed search path`);\n return null;\n }\n\n fno = path.parse(filename);\n if (os.platform() === 'win32' && fno.root.match(/[A-Za-z]:/)) {\n /* put drive letters into a virtual mount point */\n let newDir = '/mnt/' + fno.root[0].toLowerCase();\n if (fno.dir != fno.root)\n newDir += '/' + fno.dir.slice(fno.root.length);\n fno.root = '/';\n fno.dir = newDir;\n }\n /* root is always /\n * dir always starts with a / unless its empty and does not end in a /\n * base never starts with a / and does not end in a /\n */ \n canonical = fno.root;\n if (fno.dir)\n canonical += fno.dir.slice(fno.root.length);\n canonical += '/' + fno.name;\n\n if (doPathPrivacy) {\n let mmRE = new RegExp(`^${mainModuleDir}(/|$)`);\n if (canonical.match(mmRE)) {\n canonical = '/program' + canonical.slice(mainModuleDir.length);\n } else {\n if (moduleIdentifier.startsWith('./') || moduleIdentifier.startsWith('../')) {\n throw new Error(`Relative module identifiers above the program module's directory are not supported in path privacy mode (${canonical})`);\n }\n canonical = '/search' + moduleIdentifier;\n }\n }\n\n if (doPathPrivacy)\n return '/localhost' + canonical;\n else\n return canonical;\n}\n\n\n//# sourceURL=webpack:///./src/dcp-client/job/node-modules.js?");
5126
5115
 
5127
5116
  /***/ }),
5128
5117
 
@@ -5133,7 +5122,7 @@ eval("/**\n * @file node-modules.js Node-specific support for sen
5133
5122
  /*! no static exports found */
5134
5123
  /***/ (function(module, exports, __webpack_require__) {
5135
5124
 
5136
- eval("/**\n * @file job/result-handle.js\n * @author Ryan Rossiter, ryan@kingsds.network\n * @date June 2020\n *\n * The ResultHandle acts as a proxy for a job's results, querying\n * internal results when available or the scheduler when the results\n * are not available locally.\n */\n\nconst { rehydrateRange } = __webpack_require__(/*! ../range-object */ \"./src/dcp-client/range-object.js\");\nconst { fetchURI } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\n\nconst RH_INTERNAL_SYMBOL = Symbol('Result Handle Internals');\n// Array methods that don't modify the array, will be applied to the ResultHandle's values object\nconst RH_ARRAY_METHODS = [\n 'slice', 'filter', 'concat', 'find', 'findIndex', 'indexOf', 'map', 'reduce', 'includes', 'toString', 'forEach'\n];\n\n/**\n * This class represents a handle on a job's results. It can be used for accessing the job's results, or for querying the scheduler to fetch results.\n * In addition to the properties and methods of this class, the following standard array methods are also available for accessing the available results: `slice`, `filter`, `concat`, `find`, `findIndex`, `indexOf`, `map`, `reduce`, `includes`, `toString`, and `forEach`.\n * The results can also be accessed by index (`results[i]`), but an error will be thrown if the result for that index is not yet available.\n * @access public\n */\nclass ResultHandle extends Array {\n constructor(job) {\n super();\n\n const { Job, JOB_INTERNAL_SYMBOL } = __webpack_require__(/*! . */ \"./src/dcp-client/job/index.js\");\n if (!(job instanceof Job)) {\n throw new TypeError('ResultHandle must be constructed from a Job object');\n }\n\n /**\n * The length of the available results array.\n * @type {number}\n * @access public\n */\n this.length = 0; // overridden by the proxy, here for JSDoc purposes\n\n this[RH_INTERNAL_SYMBOL] = {\n job,\n keys: job[JOB_INTERNAL_SYMBOL].payloadDetails.data,\n values: job[JOB_INTERNAL_SYMBOL].results,\n valuesAvailable: job[JOB_INTERNAL_SYMBOL].resultsAvailable,\n };\n\n return new Proxy(this, {\n get: (target, name) => {\n if ((typeof name === 'string' || typeof name === 'number') && Number.isInteger(parseFloat(name))) {\n let i = parseFloat(name);\n if (target.isAvailable(i)) {\n return target[RH_INTERNAL_SYMBOL].values[i];\n } else {\n throw new Error(`Result ${i} is not available. It has either not been computed or you need to fetch it.`);\n }\n } else if (name === 'length') {\n return target.getLength();\n } else if (name === 'constructor') {\n return Array.constructor;\n } else if (RH_ARRAY_METHODS.includes(name)) {\n let values = target.values();\n return values[name].bind(values);\n } else if (name === RH_INTERNAL_SYMBOL) {\n return target[RH_INTERNAL_SYMBOL];\n } else {\n // only return methods on this class, don't allow access\n // to array methods other than the whitelisted ones\n let p = target.__proto__[name];\n if (typeof p === 'function') {\n return p.bind(target);\n } else {\n return p;\n }\n }\n }\n });\n }\n\n toJSON() {\n return JSON.stringify(this.values());\n }\n\n isAvailable(index) {\n return this[RH_INTERNAL_SYMBOL].valuesAvailable[index];\n }\n\n reset() {\n // quickly empty the values and valuesAvailable array\n this[RH_INTERNAL_SYMBOL].values.length\n = this[RH_INTERNAL_SYMBOL].valuesAvailable.length = 0;\n\n this[RH_INTERNAL_SYMBOL].values.length\n = this[RH_INTERNAL_SYMBOL].valuesAvailable.length\n = this[RH_INTERNAL_SYMBOL].keys.length;\n }\n\n /**\n * Returns an array of input values. Will only return input values that have a completed result available.\n * @access public\n * @returns {Array<*>}\n */\n keys() {\n // Keys can be a RangeObject, faster to iterate over valuesAvailable\n return this[RH_INTERNAL_SYMBOL].valuesAvailable.reduce((a, v, i) => {\n if (v) a.push(this[RH_INTERNAL_SYMBOL].keys[i]);\n return a;\n }, []);\n }\n\n /**\n * Returns an array of results. Will only return results that have been received from the scheduler, if only one result is complete the array will contain one value.\n * @access public\n * @returns {Array<*>}\n */\n values() {\n return this[RH_INTERNAL_SYMBOL].values.filter((v, i) => this.isAvailable(i));\n }\n\n // Aliased as length in proxy, can't use getter because Array.length is not configurable\n getLength() {\n return this[RH_INTERNAL_SYMBOL].valuesAvailable.reduce((n, v) => (n + (v ? 1 : 0)), 0);\n }\n\n [Symbol.iterator]() {\n let index = 0;\n let values = this.values(); // use available values\n\n return {\n next: () => ({\n value: values[index++],\n done: index > values.length\n })\n };\n }\n\n /** \n * Returns an array of [input, output] pairs, in sliceNumber order.\n * Return value is undefined if the input is not an ES5 primitive.\n * @access public\n * @returns {Array<*>}\n */\n entries() {\n return this[RH_INTERNAL_SYMBOL].valuesAvailable.reduce((a, v, i) => {\n if (v) a.push([\n String(this[RH_INTERNAL_SYMBOL].keys[i]),\n this[RH_INTERNAL_SYMBOL].values[i],\n ]);\n return a;\n }, []);\n }\n\n /** \n * Returns an Object associating input and output values where the inputs are ES5 primitive types.\n * Return value is undefined if the input is not an ES5 primitive.\n * @access public\n * @returns {object}\n */\n fromEntries() {\n return this.entries().reduce((o, [k, v]) => {\n o[k] = v; return o;\n }, {});\n }\n\n /** \n * Return the nth input value/input vector \n * @access public\n * @param {number} n Index in the input set to return the value for.\n * @returns {*} Input set value\n */\n key(n) {\n return this[RH_INTERNAL_SYMBOL].keys[n];\n }\n\n /** \n * Return the value corresponding to the provided key \n * @access public\n * @param {*} key Corresponds to a value in the job's input set.\n * @returns {*} Result corresponding to the input value\n */\n lookupValue(key) {\n // use keys instead of _keys so we only lookup on available results\n let ind = this.keys().indexOf(key);\n return this.values()[ind];\n }\n\n /**\n * Sends request to scheduler to fetch results, the retrieved results will be populated on this object.\n * @param {RangeObject} [rangeObject] - not implemented, leave as undefined - range object to query results\n * @param {string} [emitEvents] - set to 'all' to get a `result` event for each result as they are added to the handle.\n * @access public\n * @emits Job#resultsUpdated\n */\n async fetch(rangeObject, emitEvents) {\n const range = rangeObject && rehydrateRange(rangeObject);\n\n const job = this[RH_INTERNAL_SYMBOL].job;\n // Fetch any existing results\n const { success, payload } = await job.resultConnection.send('fetchResult', {\n job: job.id,\n owner: job.paymentAccountKeystore.address,\n range,\n }, job.paymentAccountKeystore);\n\n // Unpack results, using fetchURI to decode/fetch reult URIs\n await Promise.all(payload.map(async r => {\n if (!r) {\n console.warn(`ResultHandle.fetch: Received result was not defined (${r}), ignoring...`);\n return;\n }\n\n this[RH_INTERNAL_SYMBOL].values[r.slice] = await fetchURI(decodeURIComponent(r.value));\n this[RH_INTERNAL_SYMBOL].valuesAvailable[r.slice] = true;\n\n if (emitEvents && (emitEvents === 'all' || !this[RH_INTERNAL_SYMBOL].valuesAvailable[r.slice])) {\n job.emit('result', { sliceNumber: r.slice, result: r.result, taskId: r.task });\n }\n }));\n\n job.emit('resultsUpdated');\n }\n\n async list(rangeObject) {\n const range = rehydrateRange(rangeObject);\n throw new Error(\"ResultHandle.list not implemented\");\n }\n\n async delete(rangeObject) {\n const range = rehydrateRange(rangeObject);\n throw new Error(\"ResultHandle.delete not implemented\");\n }\n\n async stat(rangeObject) {\n const range = rehydrateRange(rangeObject);\n throw new Error(\"ResultHandle.stat not implemented\");\n }\n}\n\nObject.assign(exports, {\n ResultHandle,\n});\n\n\n//# sourceURL=webpack:///./src/dcp-client/job/result-handle.js?");
5125
+ eval("/**\n * @file job/result-handle.js\n * @author Ryan Rossiter, ryan@kingsds.network\n * @date June 2020\n *\n * The ResultHandle acts as a proxy for a job's results, querying\n * internal results when available or the scheduler when the results\n * are not available locally.\n */\n\nconst { rehydrateRange } = __webpack_require__(/*! ../range-object */ \"./src/dcp-client/range-object.js\");\nconst protocolV4 = __webpack_require__(/*! dcp/protocol-v4 */ \"./src/protocol-v4/index.js\");\nconst wallet = __webpack_require__(/*! dcp/dcp-client/wallet */ \"./src/dcp-client/wallet/index.js\");\nconst { fetchURI } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\nconst kvin = __webpack_require__(/*! kvin */ \"./node_modules/kvin/kvin.js\");\n\n\nconst RH_INTERNAL_SYMBOL = Symbol('Result Handle Internals');\n// Array methods that don't modify the array, will be applied to the ResultHandle's values object\nconst RH_ARRAY_METHODS = [\n 'slice', 'filter', 'concat', 'find', 'findIndex', 'indexOf', 'map', 'reduce', 'includes', 'toString', 'forEach'\n];\n\n/**\n * This class represents a handle on a job's results. It can be used for accessing the job's results, or for querying the scheduler to fetch results.\n * In addition to the properties and methods of this class, the following standard array methods are also available for accessing the available results: `slice`, `filter`, `concat`, `find`, `findIndex`, `indexOf`, `map`, `reduce`, `includes`, `toString`, and `forEach`.\n * The results can also be accessed by index (`results[i]`), but an error will be thrown if the result for that index is not yet available.\n * @access public\n */\nclass ResultHandle extends Array {\n constructor(job) {\n super();\n\n const { Job, JOB_INTERNAL_SYMBOL } = __webpack_require__(/*! . */ \"./src/dcp-client/job/index.js\");\n if (!(job instanceof Job)) {\n throw new TypeError('ResultHandle must be constructed from a Job object');\n }\n\n /**\n * The length of the available results array.\n * @type {number}\n * @access public\n */\n this.length = 0; // overridden by the proxy, here for JSDoc purposes\n\n this[RH_INTERNAL_SYMBOL] = {\n job,\n keys: job[JOB_INTERNAL_SYMBOL].payloadDetails.data,\n values: job[JOB_INTERNAL_SYMBOL].results,\n valuesAvailable: job[JOB_INTERNAL_SYMBOL].resultsAvailable,\n };\n\n return new Proxy(this, {\n get: (target, name) => {\n if ((typeof name === 'string' || typeof name === 'number') && Number.isInteger(parseFloat(name))) {\n let i = parseFloat(name);\n if (target.isAvailable(i)) {\n return target[RH_INTERNAL_SYMBOL].values[i];\n } else {\n throw new Error(`Result ${i} is not available. It has either not been computed or you need to fetch it.`);\n }\n } else if (name === 'length') {\n return target.getLength();\n } else if (name === 'constructor') {\n return Array.constructor;\n } else if (RH_ARRAY_METHODS.includes(name)) {\n let values = target.values();\n return values[name].bind(values);\n } else if (name === RH_INTERNAL_SYMBOL) {\n return target[RH_INTERNAL_SYMBOL];\n } else {\n // only return methods on this class, don't allow access\n // to array methods other than the whitelisted ones\n let p = target.__proto__[name];\n if (typeof p === 'function') {\n return p.bind(target);\n } else {\n return p;\n }\n }\n }\n });\n }\n\n toJSON() {\n return JSON.stringify(this.values());\n }\n\n isAvailable(index) {\n return this[RH_INTERNAL_SYMBOL].valuesAvailable[index];\n }\n\n reset() {\n // quickly empty the values and valuesAvailable array\n this[RH_INTERNAL_SYMBOL].values.length\n = this[RH_INTERNAL_SYMBOL].valuesAvailable.length = 0;\n\n this[RH_INTERNAL_SYMBOL].values.length\n = this[RH_INTERNAL_SYMBOL].valuesAvailable.length\n = this[RH_INTERNAL_SYMBOL].keys.length;\n }\n\n /**\n * Returns an array of input values. Will only return input values that have a completed result available.\n * @access public\n * @returns {Array<*>}\n */\n keys() {\n // Keys can be a RangeObject, faster to iterate over valuesAvailable\n return this[RH_INTERNAL_SYMBOL].valuesAvailable.reduce((a, v, i) => {\n if (v) a.push(this[RH_INTERNAL_SYMBOL].keys[i]);\n return a;\n }, []);\n }\n\n /**\n * Returns an array of results. Will only return results that have been received from the scheduler, if only one result is complete the array will contain one value.\n * @access public\n * @returns {Array<*>}\n */\n values() {\n return this[RH_INTERNAL_SYMBOL].values.filter((v, i) => this.isAvailable(i));\n }\n\n // Aliased as length in proxy, can't use getter because Array.length is not configurable\n getLength() {\n return this[RH_INTERNAL_SYMBOL].valuesAvailable.reduce((n, v) => (n + (v ? 1 : 0)), 0);\n }\n\n [Symbol.iterator]() {\n let index = 0;\n let values = this.values(); // use available values\n\n return {\n next: () => ({\n value: values[index++],\n done: index > values.length\n })\n };\n }\n\n /** \n * Returns an array of [input, output] pairs, in sliceNumber order.\n * Return value is undefined if the input is not an ES5 primitive.\n * @access public\n * @returns {Array<*>}\n */\n entries() {\n return this[RH_INTERNAL_SYMBOL].valuesAvailable.reduce((a, v, i) => {\n if (v) a.push([\n String(this[RH_INTERNAL_SYMBOL].keys[i]),\n this[RH_INTERNAL_SYMBOL].values[i],\n ]);\n return a;\n }, []);\n }\n\n /** \n * Returns an Object associating input and output values where the inputs are ES5 primitive types.\n * Return value is undefined if the input is not an ES5 primitive.\n * @access public\n * @returns {object}\n */\n fromEntries() {\n return this.entries().reduce((o, [k, v]) => {\n o[k] = v; return o;\n }, {});\n }\n\n /** \n * Return the nth input value/input vector \n * @access public\n * @param {number} n Index in the input set to return the value for.\n * @returns {*} Input set value\n */\n key(n) {\n return this[RH_INTERNAL_SYMBOL].keys[n];\n }\n\n /** \n * Return the value corresponding to the provided key \n * @access public\n * @param {*} key Corresponds to a value in the job's input set.\n * @returns {*} Result corresponding to the input value\n */\n lookupValue(key) {\n // use keys instead of _keys so we only lookup on available results\n let ind = this.keys().indexOf(key);\n return this.values()[ind];\n }\n\n /**\n * Sends request to scheduler to fetch results, the retrieved results will be populated on this object.\n * @param {RangeObject} [rangeObject] - range object to query results\n * @param {string} [emitEvents] - set to 'all' to get a `result` event for each result as they are added to the handle.\n * @access public\n * @emits Job#resultsUpdated\n */\n async fetch(rangeObject, emitEvents) {\n const range = rangeObject && rehydrateRange(rangeObject);\n\n const job = this[RH_INTERNAL_SYMBOL].job;\n \n // Fetch any existing results\n let ks = await wallet.getId();\n const conn = new protocolV4.Connection(dcpConfig.scheduler.services.resultSubmitter.location, ks);\n\n const { success, payload } = await conn.send('fetchResult', {\n job: job.id,\n owner: job.paymentAccountKeystore.address,\n range,\n }, job.paymentAccountKeystore);\n\n // Unpack results, using fetchURI to decode/fetch reult URIs\n await Promise.all(payload.map(async r => {\n if (!r) {\n console.warn(`ResultHandle.fetch: Received result was not defined (${r}), ignoring...`);\n return;\n }\n\n this[RH_INTERNAL_SYMBOL].values[r.slice] = kvin.unmarshal(await fetchURI(decodeURIComponent(r.value)));\n this[RH_INTERNAL_SYMBOL].valuesAvailable[r.slice] = true;\n\n if (emitEvents && (emitEvents === 'all' || !this[RH_INTERNAL_SYMBOL].valuesAvailable[r.slice])) {\n job.emit('result', { sliceNumber: r.slice, result: r.result, taskId: r.task });\n }\n }));\n\n job.emit('resultsUpdated');\n }\n\n async list(rangeObject) {\n const range = rehydrateRange(rangeObject);\n throw new Error(\"ResultHandle.list not implemented\");\n }\n\n async delete(rangeObject) {\n const range = rehydrateRange(rangeObject);\n throw new Error(\"ResultHandle.delete not implemented\");\n }\n\n async stat(rangeObject) {\n const range = rehydrateRange(rangeObject);\n throw new Error(\"ResultHandle.stat not implemented\");\n }\n}\n\nObject.assign(exports, {\n ResultHandle,\n});\n\n\n//# sourceURL=webpack:///./src/dcp-client/job/result-handle.js?");
5137
5126
 
5138
5127
  /***/ }),
5139
5128
 
@@ -5155,7 +5144,18 @@ eval("/**\n * @file job/slice-payment-offer.js\n * @author Ryan Ross
5155
5144
  /*! no static exports found */
5156
5145
  /***/ (function(module, exports, __webpack_require__) {
5157
5146
 
5158
- eval("/**\n * @file range-object.js\n * @author Eddie Roosenmaallen, eddie@kingsds.network\n * Matthew Palma, mpalma@kingsds.network\n * @date October 2018\n * This module exports classes and methods for working with RangeObjects\n *\n * RangeObject:\n * new RangeObject(start, end[, step[, group]])\n * new RangeObject({ start, end[, step[, group]] })\n * new RangeObject('[object RangeObject {start},{end},{step},{group}]')\n *\n * In the first two forms, step and group are optional, defaulting to 1. In the string form, all paramteres are required\n *\n *\n * Methods:\n * toString() Returns a string descriptor, suitable to pass to a new constructor\n * toObject() Returns a vanilla ES object with the four parameters as properties\n * toArray() Resolve all values from the RangeObject, grouping if group > 1, as an array\n * nthValue(n) Look up a single value/group\n * slice([start[, end]]) Resolve a selection of values from start to end (exclusive).\n * Same semantics as Array.prototype.slice, including negative indexes\n * materialize(now) Resolve all values and store as an array (eg. for random distributions)\n *\n * Properties:\n * length Return the total number of values (or groups, if group > 1) generasted by the RangeObject\n * dist Name of the distributor function\n * values If present, the Distribution has been materialized\n * materializeOnScheduler If present and truthy, the distribution will be materialized on the scheduler before\n * instantiating tasks\n *\n * Examples:\n * bob = new RangeObject(1, 10)\n * bob.toArray() // [1,2,3,4,5,6,7,8,9,10]\n * bob.slice(-2) // [9,10]\n * fred = new RangeObject({ start:1, end: 1000, step: 3})\n * fred.toArray() // [1,4,7 (...) 994, 997, 1000]\n * fred.toObject() // { start: 1, end: 1000, step: 3, group: 1}\n * bill = new RO.RangeObject(0, 999, 3, 3)\n * rangeobject.js?1541083657962:42 Uncaught RangeError: Range must be divisible evenly into groups\n * at new RangeObject (rangeobject.js?1541083657962:42)\n * at <anonymous>:1:8\n * bill = new RO.RangeObject(0, 998, 3, 3)\n * bill.toArray() // [[0, 3, 6] (...), [990, 993, 996]]\n *\n * MultiRangeObject:\n * new MultiRangeObject(rol1, rol2, ... roln)\n * new MultiRangeObject([rol1, rol2, ... roln])\n * Each argument is either a RangeObject, an Array-like object (, or a Distribution object? NYI ~ER). The\n * values generated by the MRO will be a multi-dimentional array, where each element of the array is a vector\n * across all input ranges.\n */\n\nconst { distributor } = __webpack_require__(/*! ./stats-ranges */ \"./src/dcp-client/stats-ranges.js\");\n\n// Some utility functions to make validating the parameters easier\nconst assert = (premise, msg) => {\n if (!premise) throw new Error(msg);\n}\n\nconst assertNumeric = (val, msg) => {\n assert(typeof val === 'number', msg);\n assert(!Number.isNaN(val), msg);\n assert(Number.isFinite(val), msg);\n}\n\nconst RANGEOBJ_REGEX = /^\\[object RangeObject (-?\\d+(?:\\.\\d+)?),(-?\\d+(?:\\.\\d+)?),(-?\\d+(?:\\.\\d+)?),(\\d+|undefined)\\]$/;\n\n/** \n * Defines a consistent interface for each of the range object types\n * to inherit from, provides some array methods.\n * @access public\n */\nclass SuperRangeObject {\n constructor() {\n return new Proxy(this, {\n get: (target, name) => {\n if ((typeof name === 'string' || typeof name === 'number') && Number.isInteger(parseFloat(name))) {\n return target.nthValue(parseFloat(name));\n } else {\n return target[name];\n }\n }\n });\n }\n\n [Symbol.iterator]() {\n let index = 0;\n\n return {\n next: () => ({ value: this.nthValue(index++), done: index > this.length })\n };\n }\n\n get length() {\n return 0;\n }\n\n nthValue(n) {\n throw new Error(\"nthValue not overridden\");\n }\n\n toArray() {\n return this.slice()\n }\n\n /**\n * Generate array from range starting at value `start` and ending at value `end` via `nthValue`.\n * @param {number} [start=0] index to start slice\n * @param {number} [end] index to end slice, return rest of array if not provided.\n * @access public\n */\n slice(start, end) {\n if (typeof start === 'undefined') { start = 0 } else if (start < 0) { start += this.length }\n\n if (typeof end === 'undefined') { end = this.length } else if (end < 0) { end += (this.length) }\n\n if (end > this.length) { end = this.length }\n\n const arr = []\n for (let i = start; i < end; i++) { arr.push(this.nthValue(i)) }\n\n return arr\n }\n\n /**\n * Converts range to an Array and then calls `filter(...args)` on it.\n * @param {...any} args Same args as you would pass to Array#filter\n * @access public\n */\n filter(...args) {\n return this.toArray().filter(...args);\n }\n}\n\n/**\n * Range objects are vanilla ES objects used to describe value range sets for use by `compute.for()`.\n * The range must be increasing, i.e. `start` must be less than `end`.\n * Calculations made to derive the set of numbers in a range are carried out with `BigNumber`, \n * eg. arbitrary-precision, support. The numbers `Infinity` and `-Infinity` are not supported, and \n * the API does not differentiate between `+0` and `-0`.\n */\nclass RangeObject extends SuperRangeObject {\n /**\n * An object which represents a range of values.\n * @param {number|object} startOrObject Beginning of range, or object literal with `start` and `end` properties.\n * @param {number} end End of range\n * @param {number} [step=1] Step size in range\n * @param {number|undefined} [group] Groups in range\n * @access public\n * @extends SuperRangeObject\n * @example <caption>With implicit step size = 1.</caption>\n * let rangeObject = new RangeObject(0.5,3);\n * rangeObject.toArray();\n * // [ 0.5, 1.5, 2.5 ]\n * \n * @example <caption>With explicit step size.</caption>\n * let rangeObject = new RangeObject(1,9,3);\n * rangeObject.toArray();\n * // [ 1, 4, 7 ]\n * \n * @example <caption>With grouping.</caption>\n * let rangeObject = new RangeObject(1,9,3,2);\n * rangeObject.toArray();\n * // [ [ 1, 4 ], [ 7 ] ]\n */\n constructor (start, end, step, group) {\n super();\n if (typeof start === 'string' && start.match(RANGEOBJ_REGEX)) {\n const parts = start.match(RANGEOBJ_REGEX)\n start = {\n start: parseFloat(parts[1]),\n end: parseFloat(parts[2]),\n step: parseFloat(parts[3]),\n group: (parts[4] === 'undefined'? undefined : parseFloat(parts[4]))\n }\n }\n\n if (typeof start === 'object') {\n this.start = start.start;\n this.end = start.end;\n this.step = start.step;\n this.group = start.group;\n } else {\n this.start = start;\n this.end = end;\n this.step = step;\n this.group = group;\n }\n\n assertNumeric(this.start, `Invalid start parameter \"${this.start}\", must be numeric and finite.`);\n assertNumeric(this.end, `Invalid end parameter \"${this.end}\", must be numeric and finite.`);\n\n // Ensure step moves in the correct direction for start-end (ie, negative if end < start)\n if (typeof this.step === 'undefined') {\n this.step = Math.sign(this.end - this.start);\n } else {\n assertNumeric(this.step, `Invalid step parameter \"${this.step}\", must be numeric and finite.`);\n if ((this.step === 0 && this.start !== this.end) || Math.sign(this.step) !== Math.sign(this.end - this.start)) {\n throw new Error('RangeObject step must approach end from start.');\n }\n }\n\n if (typeof this.group !== 'undefined') {\n // group must remain undefined if not provided because no grouping should occur if not provided.\n // As per spec, even if group is 1 it should group into arrays of 1 element\n assertNumeric(this.group, `Invalid group parameter \"${this.group}\", must be numeric and finite.`);\n assert(Number.isInteger(this.group), `Invalid group parameter \"${this.group}\", must be an integer.`);\n assert(this.group > 0, `Invalid group parameter \"${this.group}\", must be greater than zero.`);\n }\n }\n\n /**\n * @typedef {object} RangeLike\n * @property {number} start\n * @property {number} end\n * @property {number} nthValue\n */\n\n /**\n * @returns {boolean}\n */\n static isRangelike (r) {\n if (r instanceof RangeObject) { return true }\n if (typeof r === 'object' &&\n typeof r.start === 'number' &&\n typeof r.end === 'number' &&\n typeof r.nthValue === 'function') { return true }\n\n return false\n }\n\n /**\n * @returns {boolean}\n */\n static isRangeObject(r) {\n if (r instanceof RangeObject) { return true }\n\n return false\n }\n\n /**\n * Test whether a value can be passed to the RangeObject constructor\n * @param r Value to test\n * @param strict Optional. Truthy to disallow objects which already look Rangelike\n */\n static isProtoRangelike (r, strict = true) {\n if (typeof r === 'object' &&\n typeof r.start === 'number' &&\n typeof r.end === 'number') { return true }\n\n if (typeof r === 'string' &&\n r.match(RANGEOBJ_REGEX)) { return true }\n\n if (!strict && RangeObject.isRangelike(r)) { return true }\n\n return false\n }\n\n /**\n * Create string representation of range: [object RangeObject start,end,step,group]\n * @access public\n */\n toString () {\n return `[object RangeObject ${this.start},${this.end},${this.step},${this.group}]`\n }\n\n /**\n * Create object literal for range with properties: start, end, step, and group.\n * @access public\n */\n toObject () {\n return {\n start: this.start,\n end: this.end,\n step: this.step,\n group: this.group\n }\n }\n\n /**\n * Return nth value in range\n * @param {number} n\n * @access public\n * @example\n * let rangeObject = new RangeObject(1,10,3,2);\n * rangeObject.toArray();\n * // [ [ 1, 4 ], [ 7, 10 ] ]\n * rangeObject.nthValue(1);\n * // [ 7, 10 ]\n */\n nthValue(n) {\n /**\n * `>=` since the value at index 7 in an array that's of length 7 is outside\n * its range\n */\n if (n < 0 || n >= this.length) {\n return undefined;\n }\n\n if (typeof this.group !== 'undefined') {\n const start = (this.group * this.step * n) + this.start\n const arr = []\n\n for (let i = 0; i < this.group && i + this.group * n < this.stepCount; i++) {\n arr.push(start + (i * this.step))\n }\n\n return arr;\n }\n\n return this.start + (n * this.step);\n }\n\n /**\n * Return max value in range\n * @access public\n * @returns {number}\n * @example\n * let rangeObject = new RangeObject(1,10,3,2);\n * rangeObject.toArray();\n * // [ [ 1, 4 ], [ 7, 10 ] ]\n * rangeObject.max;\n * // 10\n */\n get max () {\n if (typeof this.group === 'undefined' && this.step === 1) { return this.end }\n\n let tail\n if (typeof this.group === 'undefined') {\n [ tail ] = this.slice(-1)\n } else {\n [ tail ] = this.slice(-1)[0].slice(-1)\n }\n return tail\n }\n\n /**\n * Boolean indicating whether all groups are filled.\n * Only defined for RangeObjects that group results.\n * @access public\n * @returns {boolean}\n * @example <caption>With remainder</caption>\n * let rangeObject = new RangeObject(1,9,3,2);\n * rangeObject.toArray();\n * // [ [ 1, 4 ], [ 7 ] ]\n * rangeObject.hasRemainder;\n * // true\n * @example <caption>Without remainder</caption>\n * let rangeObject = new RangeObject(1,10,3,2);\n * rangeObject.toArray();\n * // [ [ 1, 4 ], [ 7, 10 ] ]\n * rangeObject.hasRemainder;\n * // true\n */\n get hasRemainder () {\n if (typeof this.group === 'undefined') { return false }\n const groups = this.stepCount / this.group;\n\n return (groups !== Math.floor(groups));\n }\n\n /**\n * Number of elements in range, or number of groups if grouped.\n * @access public\n * @returns {number}\n * @example <caption>Without grouping</caption>\n * let rangeObject = new RangeObject(1,10,3);\n * rangeObject.toArray();\n * // [ 1, 4, 7, 10 ]\n * rangeObject.length;\n * // 4\n * @example <caption>With grouping</caption>\n * let rangeObject = new RangeObject(1,9,3,2);\n * rangeObject.toArray();\n * // [ [ 1, 4 ], [ 7 ] ]\n * rangeObject.length;\n * // 2\n */\n get length () {\n return Math.ceil(this.stepCount / (this.group || 1));\n }\n\n /**\n * Number of steps in range (sort of like number of elements, except grouping is no longer relevant).\n * @access public\n * @returns {number}\n * @example <caption>Without grouping</caption>\n * let rangeObject = new RangeObject(1,10,3);\n * rangeObject.toArray();\n * // [ 1, 4, 7, 10 ]\n * rangeObject.stepCount;\n * // 4\n * @example <caption>With grouping</caption>\n * let rangeObject = new RangeObject(1,9,3,2);\n * rangeObject.toArray();\n * // [ [ 1, 4 ], [ 7 ] ]\n * rangeObject.length;\n * // 3\n */\n get stepCount () {\n if (this.step === 0) return 1;\n return Math.floor(Math.abs((this.end - this.start) / this.step)) + 1;\n }\n}\n\n/**\n * Range objects are vanilla ES objects used to describe value range sets for use by `compute.for()`. \n * Calculations made to derive the set of numbers in a range are carried out with `BigNumber`, \n * eg. arbitrary-precision, support. The numbers `Infinity` and `-Infinity` are not supported, and \n * the API does not differentiate between `+0` and `-0`.\n */\nclass MultiRangeObject extends SuperRangeObject {\n /**\n * A multi-range object contains many {@link RangeObject}s. They are iterated over \n * with the fastest moving index going over the right-most range object in array order. Each element\n * of a multi range is a tuple of values from constituent ranges.\n * @param {RangeObject|RangeObject[]|object} arg - First range object, or array of range objects, or object with `ranges` key containing an array of range objects.\n * @param {RangeObject} rangeObject - If first argument is a RangeObject, subsquent arguments are range objects too.\n * @access public\n * @extends SuperRangeObject\n * @example\n * r0 = new RangeObject(1,2)\n * r1 = new RangeObject(1,3)\n * mro = new MultiRangeObject(r0, r1)\n * mro.toArray()\n * // [ [ 1, 1 ], [ 1, 2 ], [ 1, 3 ], [ 2, 1 ], [ 2, 2 ], [ 2, 3 ] ]\n */\n constructor () {\n super();\n var ranges = []\n\n if (arguments.length === 1 && typeof arguments[0] === 'string') {\n const inputs = JSON.parse(arguments[0])\n if (Array.isArray(inputs)) {\n ranges = inputs\n } else if (inputs.ranges) {\n ranges = inputs.ranges\n } else {\n ranges = [inputs]\n }\n } else if (arguments.length === 1 && Array.isArray(arguments[0])) {\n ranges = [...arguments[0]]\n } else if (arguments.length === 1 && !!arguments[0].ranges) {\n ranges = [...arguments[0].ranges]\n } else {\n ranges = [...arguments]\n }\n\n this.ranges = ranges.map(r => {\n if (RangeObject.isRangelike(r)) { return r }\n if (RangeObject.isRangeObject(r)) { return r }\n if (DistributionRange.isDistribution(r)) { return r }\n if (RangeObject.isProtoRangelike(r)) { return new RangeObject(r) }\n if (DistributionRange.isProtoDistribution(r)) { return new DistributionRange(r) }\n\n return Array.isArray(r) ? r : [r]\n })\n }\n\n /**\n * Test whether a value can be passed to the MultiRangeObject constructor\n * @param r Value to test\n * @param strict Optional. Truthy to disallow objects which already look Rangelike\n */\n static isProtoMultiRangelike (r, strict = true) {\n if (typeof r === 'object' &&\n Array.isArray(r.ranges)) { return true }\n\n return false\n }\n\n /**\n * Create string representation of this MultiRangeObject\n * @access public\n * @example\n * \"[object MultiRangeObject ' + this.ranges.length + ']\"\n */\n toString () {\n return '[object MultiRangeObject ' + this.ranges.length + ']'\n }\n\n /**\n * Create object literal with `ranges` property containing array of range objects.\n */\n toObject () {\n return { ranges: this.ranges }\n }\n\n /**\n * Returns a tuple of values from the ranges given by this multi range object.\n * @param {number} n index of multi-range tuple to return\n * @access public\n * @example\n * r0 = new RangeObject(1,2)\n * r1 = new RangeObject(1,3)\n * mro = new MultiRangeObject(r0, r1)\n * mro.toArray()\n * // [ [ 1, 1 ], [ 1, 2 ], [ 1, 3 ], [ 2, 1 ], [ 2, 2 ], [ 2, 3 ] ]\n * mro.nthValue(2)\n * // [ 1, 3 ]\n */\n nthValue (n) {\n if (n < 0 || n >= this.length) { return undefined }\n\n const indexes = []\n\n for (let r = (this.ranges.length - 1); r >= 0; r--) {\n const idx = n % this.ranges[r].length\n\n indexes.unshift(idx)\n\n n -= idx\n n /= this.ranges[r].length\n }\n\n const values = []\n\n for (let i = 0; i < indexes.length; i++) {\n values[i] = Array.isArray(this.ranges[i]) ? this.ranges[i][indexes[i]] : this.ranges[i].nthValue(indexes[i])\n }\n\n return values\n }\n\n /**\n * Boolean indicating whether any of the ranges in this multi-range object has a remainder. See {@link RangeObject#hasRemainder}.\n * @access public\n * @returns {boolean}\n */\n get hasRemainder () {\n for (let r of this.ranges) {\n if (r.hasRemainder) { return true }\n }\n\n return false\n }\n\n get length () {\n let len = 1\n\n this.ranges.forEach((r) => len *= r.length)\n\n return len\n }\n}\n\n// DistributionRange object wraps a distributing function into a RangeObject-like API\n// which can be dropped directly into a MultiRangeObject to generate input slices\nclass DistributionRange extends SuperRangeObject {\n constructor (n, dist, ...params) {\n super();\n // If argv[0] is a string formatted as DistributionRange.toString(), then unpack it\n if (typeof n === 'string' && n.match(/^\\[object DistributionRange (\\w+?)\\((\\d+?)(?:,(.+?))?\\)\\]$/)) {\n const parts = n.match(/^\\[object DistributionRange (\\w+?)\\((\\d+?)(?:,(.+?))?\\)\\]$/)\n dist = parts[1]\n n = parseInt(parts[2])\n params = (parts[3] || '').split(',').map(e => parseFloat(e))\n }\n\n // If argv[0] is a string describing a DistributionRange, then unpack it\n if (typeof n === 'string' && n.match(/^(\\w+?)\\((\\d+?)(?:,(.+?))?\\)$/)) {\n const parts = n.match(/^(\\w+?)\\((\\d+?)(?:,(.+?))?\\)$/)\n dist = parts[1]\n n = parseInt(parts[2])\n params = (parts[3] || '').split(',').map(e => parseFloat(e))\n }\n\n // If argv[0] is a object of the right shape, then unpack it\n if (typeof n === 'object' &&\n typeof n.length === 'number' &&\n typeof n.dist === 'string' &&\n Array.isArray(n.params)) {\n // console.log('Unpacking proto-object', n)\n dist = n.dist\n params = n.params\n n = n.length\n if (Array.isArray(n.values)) { this.values = n.values }\n if (typeof n.materializeOnScheduler === 'boolean') { this.materializeOnScheduler = n.materializeOnScheduler }\n }\n\n Object.defineProperty(this, 'length', {\n value: n,\n enumerable: true\n })\n Object.defineProperty(this, 'dist', {\n value: dist,\n enumerable: true\n })\n Object.defineProperty(this, 'params', {\n value: params || [],\n enumerable: true\n })\n\n if (typeof distributor[dist] !== 'function') {\n // console.log({n,dist,params})\n throw new TypeError('dist param must point to an exported distributing function')\n }\n }\n\n /**\n * @returns {boolean}\n */\n static isDistribution (d) {\n return d instanceof DistributionRange\n }\n\n static isDistributionLike (d) {\n if (DistributionRange.isDistribution(d)) { return true }\n if (typeof d === 'object' &&\n typeof d.nthValue === 'function' &&\n typeof d.slice === 'function') { return true }\n\n return false\n }\n\n static isProtoDistribution (d) {\n if (typeof d === 'string' && d.match(/^\\[object DistributionRange (\\w+?)\\((\\d+?)(?:,(.+?))?\\)\\]$/)) { return true }\n if (typeof d === 'string' && d.match(/^(\\w+?)\\((\\d+?)(?:,(.+?))?\\)$/)) { return true }\n if (typeof d === 'object' &&\n typeof d.length === 'number' &&\n typeof d.dist === 'string' &&\n Array.isArray(d.params)) { return true }\n\n return false\n }\n\n toString () {\n return `[object DistributionRange ${this.dist}(${[this.length, ...this.params].join()})]`\n }\n\n toObject () {\n return {\n length: this.length,\n dist: this.dist,\n params: this.params,\n materializeOnScheduler: this.materializeOnScheduler || undefined,\n values: this.values || undefined\n }\n }\n\n nthValue (n) {\n if (n < 0 || n >= this.length) { return undefined }\n\n if (this.values) { return this.values[n] }\n\n const fn = distributor[this.dist]\n\n if (typeof fn === 'function') { return fn.apply(fn, [n, this.length, ...this.params]) }\n\n return undefined\n }\n\n /** Resolve the distribution to a static array\n * @param now If false, then set a flag to materialize on the scheduler. Default: materialize now\n */\n materialize (now = true) {\n if (now === false) { return this.materializeOnScheduler = true }\n\n this.values = this.toArray()\n }\n}\n\n/** Rehydrate an input range from a vanilla ES5 object to an appropriate rangelike object\n * @param obj Serialized job.data object (or JSON string)\n * @return as appropriate, a RangeObject, DistributionRange, MultiRangeObject, or array\n */\nfunction rehydrateRange (obj) {\n if (typeof obj === 'string') {\n obj = JSON.parse(obj)\n }\n\n if (typeof obj === 'number') {\n return obj\n }\n\n if (obj instanceof RangeObject ||\n obj instanceof MultiRangeObject ||\n obj instanceof DistributionRange) {\n return obj;\n }\n\n // If obj is an iterable, coerce it to an array\n if (Symbol.iterator in Object(obj)) {\n return Array.from(obj)\n }\n\n // If obj looks like a MultiRangeObject, make one of those\n if (MultiRangeObject.isProtoMultiRangelike(obj)) {\n return new MultiRangeObject(obj)\n }\n\n // If obj looks rangelike, make a RangeObject\n if (RangeObject.isProtoRangelike(obj)) {\n return new RangeObject(obj)\n }\n\n // If obj looks like a proto-distribution, make a DistributionRange\n if (DistributionRange.isProtoDistribution(obj)) {\n return new DistributionRange(obj)\n }\n\n throw new TypeError(`obj cannot be cast to any supported Rangelike object: ${JSON.stringify(obj)}`)\n}\n\nexports.SuperRangeObject = SuperRangeObject;\nexports.RangeObject = RangeObject;\nexports.MultiRangeObject = MultiRangeObject;\nexports.DistributionRange = DistributionRange;\nexports.rehydrateRange = rehydrateRange;\n\n\n//# sourceURL=webpack:///./src/dcp-client/range-object.js?");
5147
+ eval("/**\n * @file range-object.js\n * @author Eddie Roosenmaallen, eddie@kingsds.network\n * Matthew Palma, mpalma@kingsds.network\n * @date October 2018\n * This module exports classes and methods for working with RangeObjects\n *\n * RangeObject:\n * new RangeObject(start, end[, step[, group]])\n * new RangeObject({ start, end[, step[, group]] })\n * new RangeObject('[object RangeObject {start},{end},{step},{group}]')\n *\n * In the first two forms, step and group are optional, defaulting to 1. In the string form, all paramteres are required\n *\n *\n * Methods:\n * toString() Returns a string descriptor, suitable to pass to a new constructor\n * toObject() Returns a vanilla ES object with the four parameters as properties\n * toArray() Resolve all values from the RangeObject, grouping if group > 1, as an array\n * nthValue(n) Look up a single value/group\n * slice([start[, end]]) Resolve a selection of values from start to end (exclusive).\n * Same semantics as Array.prototype.slice, including negative indexes\n * materialize(now) Resolve all values and store as an array (eg. for random distributions)\n *\n * Properties:\n * length Return the total number of values (or groups, if group > 1) generasted by the RangeObject\n * dist Name of the distributor function\n * values If present, the Distribution has been materialized\n * materializeOnScheduler If present and truthy, the distribution will be materialized on the scheduler before\n * instantiating tasks\n *\n * Examples:\n * bob = new RangeObject(1, 10)\n * bob.toArray() // [1,2,3,4,5,6,7,8,9,10]\n * bob.slice(-2) // [9,10]\n * fred = new RangeObject({ start:1, end: 1000, step: 3})\n * fred.toArray() // [1,4,7 (...) 994, 997, 1000]\n * fred.toObject() // { start: 1, end: 1000, step: 3, group: 1}\n * bill = new RO.RangeObject(0, 999, 3, 3)\n * rangeobject.js?1541083657962:42 Uncaught RangeError: Range must be divisible evenly into groups\n * at new RangeObject (rangeobject.js?1541083657962:42)\n * at <anonymous>:1:8\n * bill = new RO.RangeObject(0, 998, 3, 3)\n * bill.toArray() // [[0, 3, 6] (...), [990, 993, 996]]\n *\n * MultiRangeObject:\n * new MultiRangeObject(rol1, rol2, ... roln)\n * new MultiRangeObject([rol1, rol2, ... roln])\n * Each argument is either a RangeObject, an Array-like object (, or a Distribution object? NYI ~ER). The\n * values generated by the MRO will be a multi-dimentional array, where each element of the array is a vector\n * across all input ranges.\n */\n\nconst { distributor } = __webpack_require__(/*! ./stats-ranges */ \"./src/dcp-client/stats-ranges.js\");\n\n// Some utility functions to make validating the parameters easier\nconst assert = (premise, msg) => {\n if (!premise) throw new Error(msg);\n}\n\nconst assertNumeric = (val, msg) => {\n assert(typeof val === 'number', msg);\n assert(!Number.isNaN(val), msg);\n assert(Number.isFinite(val), msg);\n}\n\nconst RANGEOBJ_REGEX = /^\\[object RangeObject (-?\\d+(?:\\.\\d+)?),(-?\\d+(?:\\.\\d+)?),(-?\\d+(?:\\.\\d+)?),(\\d+|undefined)\\]$/;\n\n/** \n * Defines a consistent interface for each of the range object types\n * to inherit from, provides some array methods.\n * @access public\n */\nclass SuperRangeObject {\n constructor() {\n return new Proxy(this, {\n get: (target, name) => {\n if ((typeof name === 'string' || typeof name === 'number') && Number.isInteger(parseFloat(name))) {\n return target.nthValue(parseFloat(name));\n } else {\n return target[name];\n }\n }\n });\n }\n\n [Symbol.iterator]() {\n let index = 0;\n\n return {\n next: () => ({ value: this.nthValue(index++), done: index > this.length })\n };\n }\n\n get length() {\n return 0;\n }\n\n nthValue(n) {\n throw new Error(\"nthValue not overridden\");\n }\n\n toArray() {\n return this.slice()\n }\n\n /**\n * Generate array from range starting at value `start` and ending at value `end` via `nthValue`.\n * @param {number} [start=0] index to start slice\n * @param {number} [end] index to end slice, return rest of array if not provided.\n * @access public\n */\n slice(start, end) {\n if (typeof start === 'undefined') { start = 0 } else if (start < 0) { start += this.length }\n\n if (typeof end === 'undefined') { end = this.length } else if (end < 0) { end += (this.length) }\n\n if (end > this.length) { end = this.length }\n\n const arr = []\n for (let i = start; i < end; i++) { arr.push(this.nthValue(i)) }\n\n return arr\n }\n\n /**\n * Converts range to an Array and then calls `filter(...args)` on it.\n * @param {...any} args Same args as you would pass to Array#filter\n * @access public\n */\n filter(...args) {\n return this.toArray().filter(...args);\n }\n}\n\n/**\n * Range objects are vanilla ES objects used to describe value range sets for use by `compute.for()`.\n * The range must be increasing, i.e. `start` must be less than `end`.\n * Calculations made to derive the set of numbers in a range are carried out with `BigNumber`, \n * eg. arbitrary-precision, support. The numbers `Infinity` and `-Infinity` are not supported, and \n * the API does not differentiate between `+0` and `-0`.\n */\nclass RangeObject extends SuperRangeObject {\n /**\n * An object which represents a range of values.\n * @param {number|object} startOrObject Beginning of range, or object literal with `start` and `end` properties.\n * @param {number} end End of range\n * @param {number} [step=1] Step size in range\n * @param {number|undefined} [group] Groups in range\n * @access public\n * @extends SuperRangeObject\n * @example <caption>With implicit step size = 1.</caption>\n * let rangeObject = new RangeObject(0.5,3);\n * rangeObject.toArray();\n * // [ 0.5, 1.5, 2.5 ]\n * \n * @example <caption>With explicit step size.</caption>\n * let rangeObject = new RangeObject(1,9,3);\n * rangeObject.toArray();\n * // [ 1, 4, 7 ]\n * \n * @example <caption>With grouping.</caption>\n * let rangeObject = new RangeObject(1,9,3,2);\n * rangeObject.toArray();\n * // [ [ 1, 4 ], [ 7 ] ]\n */\n constructor (start, end, step, group) {\n super();\n if (typeof start === 'string' && start.match(RANGEOBJ_REGEX)) {\n const parts = start.match(RANGEOBJ_REGEX)\n start = {\n start: parseFloat(parts[1]),\n end: parseFloat(parts[2]),\n step: parseFloat(parts[3]),\n group: (parts[4] === 'undefined'? undefined : parseFloat(parts[4]))\n }\n }\n\n if (typeof start === 'object') {\n this.start = start.start;\n this.end = start.end;\n this.step = start.step;\n this.group = start.group;\n } else {\n this.start = start;\n this.end = end;\n this.step = step;\n this.group = group;\n }\n\n assertNumeric(this.start, `Invalid start parameter \"${this.start}\", must be numeric and finite.`);\n assertNumeric(this.end, `Invalid end parameter \"${this.end}\", must be numeric and finite.`);\n\n // Ensure step moves in the correct direction for start-end (ie, negative if end < start)\n if (typeof this.step === 'undefined') {\n this.step = Math.sign(this.end - this.start);\n } else {\n assertNumeric(this.step, `Invalid step parameter \"${this.step}\", must be numeric and finite.`);\n if ((this.step === 0 && this.start !== this.end) || Math.sign(this.step) !== Math.sign(this.end - this.start)) {\n throw new Error('RangeObject step must approach end from start.');\n }\n }\n\n if (typeof this.group !== 'undefined') {\n // group must remain undefined if not provided because no grouping should occur if not provided.\n // As per spec, even if group is 1 it should group into arrays of 1 element\n assertNumeric(this.group, `Invalid group parameter \"${this.group}\", must be numeric and finite.`);\n assert(Number.isInteger(this.group), `Invalid group parameter \"${this.group}\", must be an integer.`);\n assert(this.group > 0, `Invalid group parameter \"${this.group}\", must be greater than zero.`);\n }\n }\n\n /**\n * @typedef {object} RangeLike\n * @property {number} start\n * @property {number} end\n * @property {number} nthValue\n */\n\n /**\n * @returns {boolean}\n */\n static isRangelike (r) {\n if (r instanceof RangeObject) { return true }\n if (typeof r === 'object' &&\n typeof r.start === 'number' &&\n typeof r.end === 'number' &&\n typeof r.nthValue === 'function') { return true }\n\n return false\n }\n\n /**\n * @returns {boolean}\n */\n static isRangeObject(r) {\n if (r instanceof RangeObject) { return true }\n\n return false\n }\n\n /**\n * Test whether a value can be passed to the RangeObject constructor\n * @param r Value to test\n * @param strict Optional. Truthy to disallow objects which already look Rangelike\n */\n static isProtoRangelike (r, strict = true) {\n if (typeof r === 'object' &&\n typeof r.start === 'number' &&\n typeof r.end === 'number') { return true }\n\n if (typeof r === 'string' &&\n r.match(RANGEOBJ_REGEX)) { return true }\n\n if (!strict && RangeObject.isRangelike(r)) { return true }\n\n return false\n }\n\n /**\n * Create string representation of range: [object RangeObject start,end,step,group]\n * @access public\n */\n toString () {\n return `[object RangeObject ${this.start},${this.end},${this.step},${this.group}]`\n }\n\n /**\n * Create object literal for range with properties: start, end, step, and group.\n * @access public\n */\n toObject () {\n return {\n start: this.start,\n end: this.end,\n step: this.step,\n group: this.group\n }\n }\n\n /**\n * Return nth value in range\n * @param {number} n\n * @access public\n * @example\n * let rangeObject = new RangeObject(1,10,3,2);\n * rangeObject.toArray();\n * // [ [ 1, 4 ], [ 7, 10 ] ]\n * rangeObject.nthValue(1);\n * // [ 7, 10 ]\n */\n nthValue(n) {\n /**\n * `>=` since the value at index 7 in an array that's of length 7 is outside\n * its range\n */\n if (n < 0 || n >= this.length) {\n return undefined;\n }\n\n if (typeof this.group !== 'undefined') {\n const start = (this.group * this.step * n) + this.start\n const arr = []\n\n for (let i = 0; i < this.group && i + this.group * n < this.stepCount; i++) {\n arr.push(start + (i * this.step))\n }\n\n return arr;\n }\n\n return this.start + (n * this.step);\n }\n\n /**\n * Return max value in range\n * @access public\n * @returns {number}\n * @example\n * let rangeObject = new RangeObject(1,10,3,2);\n * rangeObject.toArray();\n * // [ [ 1, 4 ], [ 7, 10 ] ]\n * rangeObject.max;\n * // 10\n */\n get max () {\n if (typeof this.group === 'undefined' && this.step === 1) { return this.end }\n\n let tail\n if (typeof this.group === 'undefined') {\n [ tail ] = this.slice(-1)\n } else {\n [ tail ] = this.slice(-1)[0].slice(-1)\n }\n return tail\n }\n\n /**\n * Boolean indicating whether all groups are filled.\n * Only defined for RangeObjects that group results.\n * @access public\n * @returns {boolean}\n * @example <caption>With remainder</caption>\n * let rangeObject = new RangeObject(1,9,3,2);\n * rangeObject.toArray();\n * // [ [ 1, 4 ], [ 7 ] ]\n * rangeObject.hasRemainder;\n * // true\n * @example <caption>Without remainder</caption>\n * let rangeObject = new RangeObject(1,10,3,2);\n * rangeObject.toArray();\n * // [ [ 1, 4 ], [ 7, 10 ] ]\n * rangeObject.hasRemainder;\n * // true\n */\n get hasRemainder () {\n if (typeof this.group === 'undefined') { return false }\n const groups = this.stepCount / this.group;\n\n return (groups !== Math.floor(groups));\n }\n\n /**\n * Number of elements in range, or number of groups if grouped.\n * @access public\n * @returns {number}\n * @example <caption>Without grouping</caption>\n * let rangeObject = new RangeObject(1,10,3);\n * rangeObject.toArray();\n * // [ 1, 4, 7, 10 ]\n * rangeObject.length;\n * // 4\n * @example <caption>With grouping</caption>\n * let rangeObject = new RangeObject(1,9,3,2);\n * rangeObject.toArray();\n * // [ [ 1, 4 ], [ 7 ] ]\n * rangeObject.length;\n * // 2\n */\n get length () {\n return Math.ceil(this.stepCount / (this.group || 1));\n }\n\n /**\n * Number of steps in range (sort of like number of elements, except grouping is no longer relevant).\n * @access public\n * @returns {number}\n * @example <caption>Without grouping</caption>\n * let rangeObject = new RangeObject(1,10,3);\n * rangeObject.toArray();\n * // [ 1, 4, 7, 10 ]\n * rangeObject.stepCount;\n * // 4\n * @example <caption>With grouping</caption>\n * let rangeObject = new RangeObject(1,9,3,2);\n * rangeObject.toArray();\n * // [ [ 1, 4 ], [ 7 ] ]\n * rangeObject.length;\n * // 3\n */\n get stepCount () {\n if (this.step === 0) return 1;\n return Math.floor(Math.abs((this.end - this.start) / this.step)) + 1;\n }\n}\n\n/**\n * Range objects are vanilla ES objects used to describe value range sets for use by `compute.for()`. \n * Calculations made to derive the set of numbers in a range are carried out with `BigNumber`, \n * eg. arbitrary-precision, support. The numbers `Infinity` and `-Infinity` are not supported, and \n * the API does not differentiate between `+0` and `-0`.\n */\nclass MultiRangeObject extends SuperRangeObject {\n /**\n * A multi-range object contains many {@link RangeObject}s. They are iterated over \n * with the fastest moving index going over the right-most range object in array order. Each element\n * of a multi range is a tuple of values from constituent ranges.\n * @param {RangeObject|RangeObject[]|object} arg - First range object, or array of range objects, or object with `ranges` key containing an array of range objects.\n * @param {RangeObject} rangeObject - If first argument is a RangeObject, subsquent arguments are range objects too.\n * @access public\n * @extends SuperRangeObject\n * @example\n * r0 = new RangeObject(1,2)\n * r1 = new RangeObject(1,3)\n * mro = new MultiRangeObject(r0, r1)\n * mro.toArray()\n * // [ [ 1, 1 ], [ 1, 2 ], [ 1, 3 ], [ 2, 1 ], [ 2, 2 ], [ 2, 3 ] ]\n */\n constructor () {\n super();\n var ranges = []\n\n if (arguments.length === 1 && typeof arguments[0] === 'string') {\n const inputs = JSON.parse(arguments[0])\n if (Array.isArray(inputs)) {\n ranges = inputs\n } else if (inputs.ranges) {\n ranges = inputs.ranges\n } else {\n ranges = [inputs]\n }\n } else if (arguments.length === 1 && Array.isArray(arguments[0])) {\n ranges = [...arguments[0]]\n } else if (arguments.length === 1 && !!arguments[0].ranges) {\n ranges = [...arguments[0].ranges]\n } else {\n ranges = [...arguments]\n }\n\n this.ranges = ranges.map(r => {\n if (RangeObject.isRangelike(r)) { return r }\n if (RangeObject.isRangeObject(r)) { return r }\n if (DistributionRange.isDistribution(r)) { return r }\n if (RangeObject.isProtoRangelike(r)) { return new RangeObject(r) }\n if (DistributionRange.isProtoDistribution(r)) { return new DistributionRange(r) }\n\n return Array.isArray(r) ? r : [r]\n })\n }\n\n /**\n * Test whether a value can be passed to the MultiRangeObject constructor\n * @param r Value to test\n * @param strict Optional. Truthy to disallow objects which already look Rangelike\n */\n static isProtoMultiRangelike (r, strict = true) {\n if (typeof r === 'object' &&\n Array.isArray(r.ranges)) { return true }\n\n return false\n }\n\n /**\n * Create string representation of this MultiRangeObject\n * @access public\n * @example\n * \"[object MultiRangeObject ' + this.ranges.length + ']\"\n */\n toString () {\n return '[object MultiRangeObject ' + this.ranges.length + ']'\n }\n\n /**\n * Create object literal with `ranges` property containing array of range objects.\n */\n toObject () {\n return { ranges: this.ranges }\n }\n\n /**\n * Returns a tuple of values from the ranges given by this multi range object.\n * @param {number} n index of multi-range tuple to return\n * @access public\n * @example\n * r0 = new RangeObject(1,2)\n * r1 = new RangeObject(1,3)\n * mro = new MultiRangeObject(r0, r1)\n * mro.toArray()\n * // [ [ 1, 1 ], [ 1, 2 ], [ 1, 3 ], [ 2, 1 ], [ 2, 2 ], [ 2, 3 ] ]\n * mro.nthValue(2)\n * // [ 1, 3 ]\n */\n nthValue (n) {\n if (n < 0 || n >= this.length) { return undefined }\n\n const indexes = []\n\n for (let r = (this.ranges.length - 1); r >= 0; r--) {\n const idx = n % this.ranges[r].length\n\n indexes.unshift(idx)\n\n n -= idx\n n /= this.ranges[r].length\n }\n\n const values = []\n\n for (let i = 0; i < indexes.length; i++) {\n values[i] = Array.isArray(this.ranges[i]) ? this.ranges[i][indexes[i]] : this.ranges[i].nthValue(indexes[i])\n }\n\n return values\n }\n\n /**\n * Boolean indicating whether any of the ranges in this multi-range object has a remainder. See {@link RangeObject#hasRemainder}.\n * @access public\n * @returns {boolean}\n */\n get hasRemainder () {\n for (let r of this.ranges) {\n if (r.hasRemainder) { return true }\n }\n\n return false\n }\n\n get length () {\n let len = 1\n\n this.ranges.forEach((r) => len *= r.length)\n\n return len\n }\n}\n\n// DistributionRange object wraps a distributing function into a RangeObject-like API\n// which can be dropped directly into a MultiRangeObject to generate input slices\nclass DistributionRange extends SuperRangeObject {\n constructor (n, dist, ...params) {\n super();\n // If argv[0] is a string formatted as DistributionRange.toString(), then unpack it\n if (typeof n === 'string' && n.match(/^\\[object DistributionRange (\\w+?)\\((\\d+?)(?:,(.+?))?\\)\\]$/)) {\n const parts = n.match(/^\\[object DistributionRange (\\w+?)\\((\\d+?)(?:,(.+?))?\\)\\]$/)\n dist = parts[1]\n n = parseInt(parts[2])\n params = (parts[3] || '').split(',').map(e => parseFloat(e))\n }\n\n // If argv[0] is a string describing a DistributionRange, then unpack it\n if (typeof n === 'string' && n.match(/^(\\w+?)\\((\\d+?)(?:,(.+?))?\\)$/)) {\n const parts = n.match(/^(\\w+?)\\((\\d+?)(?:,(.+?))?\\)$/)\n dist = parts[1]\n n = parseInt(parts[2])\n params = (parts[3] || '').split(',').map(e => parseFloat(e))\n }\n\n // If argv[0] is a object of the right shape, then unpack it\n if (typeof n === 'object' &&\n typeof n.length === 'number' &&\n typeof n.dist === 'string' &&\n Array.isArray(n.params)) {\n // console.log('Unpacking proto-object', n)\n dist = n.dist\n params = n.params\n n = n.length\n if (Array.isArray(n.values)) { this.values = n.values }\n if (typeof n.materializeOnScheduler === 'boolean') { this.materializeOnScheduler = n.materializeOnScheduler }\n }\n\n Object.defineProperty(this, 'length', {\n value: n,\n enumerable: true\n })\n Object.defineProperty(this, 'dist', {\n value: dist,\n enumerable: true\n })\n Object.defineProperty(this, 'params', {\n value: params || [],\n enumerable: true\n })\n\n if (typeof distributor[dist] !== 'function') {\n // console.log({n,dist,params})\n throw new TypeError('dist param must point to an exported distributing function')\n }\n }\n\n /**\n * @returns {boolean}\n */\n static isDistribution (d) {\n return d instanceof DistributionRange\n }\n\n static isDistributionLike (d) {\n if (DistributionRange.isDistribution(d)) { return true }\n if (typeof d === 'object' &&\n typeof d.nthValue === 'function' &&\n typeof d.slice === 'function') { return true }\n\n return false\n }\n\n static isProtoDistribution (d) {\n if (typeof d === 'string' && d.match(/^\\[object DistributionRange (\\w+?)\\((\\d+?)(?:,(.+?))?\\)\\]$/)) { return true }\n if (typeof d === 'string' && d.match(/^(\\w+?)\\((\\d+?)(?:,(.+?))?\\)$/)) { return true }\n if (typeof d === 'object' &&\n typeof d.length === 'number' &&\n typeof d.dist === 'string' &&\n Array.isArray(d.params)) { return true }\n\n return false\n }\n\n toString () {\n return `[object DistributionRange ${this.dist}(${[this.length, ...this.params].join()})]`\n }\n\n toObject () {\n return {\n length: this.length,\n dist: this.dist,\n params: this.params,\n materializeOnScheduler: this.materializeOnScheduler || undefined,\n values: this.values || undefined\n }\n }\n\n nthValue (n) {\n if (n < 0 || n >= this.length) { return undefined }\n\n if (this.values) { return this.values[n] }\n\n const fn = distributor[this.dist]\n\n if (typeof fn === 'function') { return fn.apply(fn, [n, this.length, ...this.params]) }\n\n return undefined\n }\n\n /** Resolve the distribution to a static array\n * @param now If false, then set a flag to materialize on the scheduler. Default: materialize now\n */\n materialize (now = true) {\n if (now === false) { return this.materializeOnScheduler = true }\n\n this.values = this.toArray()\n }\n}\n\n/** Rehydrate an input range from a vanilla ES5 object to an appropriate rangelike object\n * @param obj Serialized job.data object (or JSON string)\n * @return as appropriate, a RangeObject, DistributionRange, MultiRangeObject, or array\n */\n const { RemoteDataPattern } = __webpack_require__(/*! dcp/dcp-client/remote-data-pattern */ \"./src/dcp-client/remote-data-pattern.js\");\n const { RemoteDataSet } = __webpack_require__(/*! dcp/dcp-client/remote-data-set */ \"./src/dcp-client/remote-data-set.js\");\n \nfunction rehydrateRange (obj) {\n if (typeof obj === 'string') {\n obj = JSON.parse(obj)\n }\n\n if (typeof obj === 'number') {\n return obj\n }\n\n if (obj instanceof RangeObject ||\n obj instanceof MultiRangeObject ||\n obj instanceof RemoteDataSet ||\n obj instanceof RemoteDataPattern ||\n obj instanceof DistributionRange) {\n return obj;\n }\n\n // If obj looks like a RemoteDataSet, make one of those\n if (RemoteDataSet.isProtoRemoteDataSetLike(obj)) {\n return new RemoteDataSet(obj)\n }\n \n // If obj looks like a RemoteDataPattern, make one of those\n if (RemoteDataPattern.isProtoRemoteDataPatternLike(obj)) {\n return new RemoteDataPattern(obj.pattern, obj.sliceCount)\n }\n\n // If obj is an iterable, coerce it to an array\n if (Symbol.iterator in Object(obj)) {\n return Array.from(obj)\n }\n\n // If obj looks like a MultiRangeObject, make one of those\n if (MultiRangeObject.isProtoMultiRangelike(obj)) {\n return new MultiRangeObject(obj)\n }\n\n // If obj looks rangelike, make a RangeObject\n if (RangeObject.isProtoRangelike(obj)) {\n return new RangeObject(obj)\n }\n\n // If obj looks like a proto-distribution, make a DistributionRange\n if (DistributionRange.isProtoDistribution(obj)) {\n return new DistributionRange(obj)\n }\n\n throw new TypeError(`obj cannot be cast to any supported Rangelike object: ${JSON.stringify(obj)}`)\n}\n\nexports.SuperRangeObject = SuperRangeObject;\nexports.RangeObject = RangeObject;\nexports.MultiRangeObject = MultiRangeObject;\nexports.DistributionRange = DistributionRange;\nexports.rehydrateRange = rehydrateRange;\n\n\n//# sourceURL=webpack:///./src/dcp-client/range-object.js?");
5148
+
5149
+ /***/ }),
5150
+
5151
+ /***/ "./src/dcp-client/remote-data-pattern.js":
5152
+ /*!***********************************************!*\
5153
+ !*** ./src/dcp-client/remote-data-pattern.js ***!
5154
+ \***********************************************/
5155
+ /*! no static exports found */
5156
+ /***/ (function(module, exports, __webpack_require__) {
5157
+
5158
+ eval("/**\n * @file remote-data-pattern.js - Contains the class definition with two arguments: pattern \n * and number of slices.\n * \n * @see {@link https://kingsds.atlassian.net/browse/DCP-2313|Jira Issue}\n * @author Nazila Akhavan <nazila@kingsds.network>\n * @date Sep. 2021\n */\n\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\")\nconst { DcpURL } = __webpack_require__(/*! dcp/common/dcp-url */ \"./src/common/dcp-url.js\");\n\nconst debugging = __webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope('dcp-client');\nconst debug = (...args) => {\n if (debugging('compute:remote-data-set')) {\n args.unshift('dcp-client:compute:remote-data-set');\n console.debug(...args);\n }\n};\n\n/**\n * Instances of this class store URIs representing job input data sets. The\n * primary purpose of this class, at this time, is to prevent repeating uri's \n * that have the similar pattern(example: 'http://site.com/1.json','http://site.com/2.json') and allow us to pass only the pattern and the\n * number of slices. Later in the requestTask, all slices based on the pattern will be fetched.\n *\n * \n * @example\n * const { RemoteDataPattern } = require('dcp/compute');\n * const pattern = 'http://site.com/{slice}.json';\n * let b = new RemoteDataPattern(pattern, 2);\n * const job = compute.for(b, function(el) { console.log(el)});\n * await job.exec();\n */\n/**\n * \n * @param {string | URL} pattern pattern of the uri's and SHOULD includes '{slice}'\n * @param {number} sliceCount total number of slices\n */\nmodule.exports.RemoteDataPattern = function (pattern, sliceCount)\n{\n let isPattern;\n\n if (typeof pattern === 'string')\n isPattern = pattern.match(/\\{slice\\}/);\n\n if (DcpURL.isURL(pattern)) {\n isPattern = pattern.href.match(/%7Bslice%7D/);\n if(isPattern)\n pattern = pattern.href.replace('%7Bslice%7D', '{slice}');\n }\n if(!isPattern || !sliceCount || typeof Number(sliceCount) !== 'number') {\n throw new DCPError(`invalid pattern/sliceCount:Note that pattern should includes \"{slice}\"\\n pattern:${pattern}, sliceCount:${sliceCount}`);\n }\n this.length = 2;\n this['pattern'] = pattern;\n this['sliceCount'] = sliceCount;\n}\n\nmodule.exports.RemoteDataPattern.isRemoteDataPattern = function (r)\n{\n if (r instanceof module.exports.RemoteDataPattern)\n return true;\n\n return false;\n}\n\nmodule.exports.RemoteDataPattern.isProtoRemoteDataPatternLike = function (r) {\n if (typeof r === 'object' && typeof r.sliceCount === 'number')\n {// pattern should include '{slice}'\n if (typeof r.pattern === 'string' && r.pattern.match(/\\{slice\\}/))\n return true;\n if (DcpURL.isURL(r.pattern) && r.pattern.href.match(/%7Bslice%7D/))\n return true;\n }\n\n return false;\n}\n\nmodule.exports.RemoteDataPattern.prototype = new Array(); \n\n//# sourceURL=webpack:///./src/dcp-client/remote-data-pattern.js?");
5159
5159
 
5160
5160
  /***/ }),
5161
5161
 
@@ -5166,7 +5166,7 @@ eval("/**\n * @file range-object.js\n * @author Eddie Roosenmaallen,
5166
5166
  /*! no static exports found */
5167
5167
  /***/ (function(module, exports, __webpack_require__) {
5168
5168
 
5169
- eval("/**\n * @file remote-data-set.js - Contains the class definition for an array of\n * URI's.\n * @author Bryan Hoang <bryan@kingsds.network>\n * @date Oct. 2020\n */\n\nconst validateDataUrl = __webpack_require__(/*! valid-data-url */ \"./node_modules/valid-data-url/index.js\");\nconst debugging = __webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope('dcp-client');\n\nconst debug = (...args) => {\n if (debugging('compute:remote-data-set')) {\n args.unshift('dcp-client:compute:remote-data-set');\n console.debug(...args);\n }\n};\n\n/**\n * Instances of this class store URIs representing data, arguments, etc. The\n * primary purpose of this class, at this time, is to allow us to easily\n * differentiate between Arrays of stuff and Arrays of URIs which point at\n * stuff. This job logs the contents of the URLs described by a URI, not the\n * URLs themselves:\n *\n * @example\n * const a = [ 'http://site.com/1.json', 'http://site.com/2.json' ];\n * const { RemoteDataSet } = require('dcp/compute');\n * const job = compute.for(new RemoteDataSet(...a), function(el) { console.log(el)});\n * job.on('console', console.log);\n * await job.exec();\n */\nmodule.exports = class RemoteDataSet extends Array {\n /**\n * @param {undefined | string[]} uris\n */\n constructor(...uris) {\n if (!RemoteDataSet.isRemoteDataSet(uris)) {\n throw new TypeError(\n 'RemoteDataSet constructor argument is not an Array of URI strings.',\n );\n }\n super(...uris);\n }\n\n /**\n * @param {string[]} uris\n */\n push(...uris) {\n if (!RemoteDataSet.isRemoteDataSet(uris)) {\n throw new TypeError('Cannot add non URI strings to a RemoteDataSet.');\n }\n return super.push(...uris);\n }\n\n /**\n * Determines if the passed in value is an array of URI strings.\n * @param {any} value\n * @returns {boolean} True if the value is an array of\n * URI strings. False otherwise.\n */\n static isRemoteDataSet(value) {\n /**\n * Determines if the value passed in is a valid URI.\n *\n * Is meant to be a callback to `Array.prototype.every`.\n *\n * @param {string} uri\n * @param {number} index\n * @returns {boolean} True if the uri is valid. False otherwise.\n */\n const isURI = (uri, index) => {\n try {\n // Check if it's a valid URL.\n if (typeof uri === 'string') {\n throw new TypeError('URIs must be passed as Objects, received String');\n }\n\n const url = new URL(uri);\n\n // If it's a data URL, validate it more strictly.\n if (url.protocol === 'data:') {\n return validateDataUrl(uri);\n }\n\n // Else the uri was parsed properly as a URL.\n return true;\n } catch (error) {\n debug(\n `Index ${index}: ${uri} failed coercion check for an element of a RemoteDataSet`,\n );\n\n return false;\n }\n };\n\n return super.isArray(value) && value.every(isURI);\n }\n};\n\n\n//# sourceURL=webpack:///./src/dcp-client/remote-data-set.js?");
5169
+ eval("/**\n * @file remote-data-set.js - Contains the class definition for (array of) URI's.\n * \n * @see {@link https://kingsds.atlassian.net/browse/DCP-1478|Jira Issue}\n * @author Bryan Hoang <bryan@kingsds.network>\n * Nazila Akhavan <nazila@kingsds.network>\n * @date Oct. 2020, Sep. 2021\n * \n */\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\")\nconst { DcpURL } = __webpack_require__(/*! dcp/common/dcp-url */ \"./src/common/dcp-url.js\");\nconst debugging = __webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope('dcp-client');\n\nconst debug = (...args) => {\n if (debugging('compute:remote-data-set')) {\n args.unshift('dcp-client:compute:remote-data-set');\n console.debug(...args);\n }\n};\n\n/**\n * Instances of this class store URIs representing data, arguments, etc. The\n * primary purpose of this class, at this time, is to allow us to easily\n * differentiate between Arrays of stuff and Arrays of URIs which point at\n * stuff. This job logs the contents of the URLs described by a URI, not the\n * URLs themselves:\n *\n * @example\n * const a = [ 'http://abc.com/1.json', 'http://def.com/two.json' ];\n * const { RemoteDataSet } = require('dcp/compute');\n * let b = new RemoteDataSet(...a);\n * OR( b = new RemoteDataSet(a))\n * const job = compute.for(b, function(el) { console.log(el)});\n * job.on('console', console.log);\n * await job.exec();\n */\nmodule.exports.RemoteDataSet = function (uris)\n{\n if (!Array.isArray(uris))\n uris = Array.from(arguments);\n this.length = uris.length;\n\n for (let i = 0; i < uris.length; i++)\n {\n if (typeof uris[i] === \"string\")\n this[i] = new URL(uris[i]).href;\n else if (DcpURL.isURL(uris[i]))\n this[i] = uris[i].href;\n else\n throw new DCPError(`invalid uri at position ${i + 1}: ${uris[i]}`);\n }\n}\n\nmodule.exports.RemoteDataSet.isRemoteDataSet = function (r)\n{\n if (r instanceof module.exports.RemoteDataSet)\n return true;\n\n return false;\n}\n\nmodule.exports.RemoteDataSet.isProtoRemoteDataSetLike = function (r)\n{\n if (Array.isArray(r))\n {\n for (let i = 0; i < r.length; i++)\n {\n if (!DcpURL.isURL(r[i]))\n return false;\n }\n return true;\n }\n\n return false;\n}\n \nmodule.exports.RemoteDataSet.prototype = new Array(); \n\n//# sourceURL=webpack:///./src/dcp-client/remote-data-set.js?");
5170
5170
 
5171
5171
  /***/ }),
5172
5172
 
@@ -5199,7 +5199,7 @@ eval("/* WEBPACK VAR INJECTION */(function(process) {/**\n * @file /src/sch
5199
5199
  /*! no static exports found */
5200
5200
  /***/ (function(module, exports, __webpack_require__) {
5201
5201
 
5202
- eval("/**\n * @file /src/schedmsg/schedmsg-web.js\n * @author Ryan Rossiter, ryan@kingsds.network\n * @date March 2020\n *\n * This is the SchedMsg implementation for commands that are browser-specific\n * or have browser-specific behaviour.\n */\n\nconst { SchedMsg } = __webpack_require__(/*! ./schedmsg */ \"./src/dcp-client/schedmsg/schedmsg.js\");\n\nclass SchedMsgWeb extends SchedMsg {\n constructor(worker) {\n super(worker);\n this.modal = null;\n\n this.registerHandler('announce', this.onAnnouncement.bind(this));\n this.registerHandler('openPopup', this.onOpenPopup.bind(this));\n this.registerHandler('reload', this.onReload.bind(this));\n }\n\n onAnnouncement({ message }) {\n if (this.modal) {\n this.modal.close();\n }\n\n this.modal = window.userInterface.alert('Announcement', '' /* subtitle */, message,\n /* onClose */ () => this.modal = null);\n }\n\n onOpenPopup({ href }) {\n window.open(href);\n }\n\n onReload() {\n const hash = window.location.hash;\n\n let newUrl = window.location.href.replace(/#.*/, '');\n newUrl += (newUrl.indexOf('?') === -1 ? '?' : '&');\n newUrl += 'dcp=dd98e423ca01eaeeba22e0bd0d948e358ffc7b43,' + Date.now() + hash;\n\n window.location.replace(newUrl);\n }\n}\n\nObject.assign(module.exports, {\n SchedMsgWeb\n});\n\n\n//# sourceURL=webpack:///./src/dcp-client/schedmsg/schedmsg-web.js?");
5202
+ eval("/**\n * @file /src/schedmsg/schedmsg-web.js\n * @author Ryan Rossiter, ryan@kingsds.network\n * @date March 2020\n *\n * This is the SchedMsg implementation for commands that are browser-specific\n * or have browser-specific behaviour.\n */\n\nconst { SchedMsg } = __webpack_require__(/*! ./schedmsg */ \"./src/dcp-client/schedmsg/schedmsg.js\");\n\nclass SchedMsgWeb extends SchedMsg {\n constructor(worker) {\n super(worker);\n this.modal = null;\n\n this.registerHandler('announce', this.onAnnouncement.bind(this));\n this.registerHandler('openPopup', this.onOpenPopup.bind(this));\n this.registerHandler('reload', this.onReload.bind(this));\n }\n\n onAnnouncement({ message }) {\n if (this.modal) {\n this.modal.close();\n }\n\n this.modal = window.userInterface.alert('Announcement', '' /* subtitle */, message,\n /* onClose */ () => this.modal = null);\n }\n\n onOpenPopup({ href }) {\n window.open(href);\n }\n\n onReload() {\n const hash = window.location.hash;\n\n let newUrl = window.location.href.replace(/#.*/, '');\n newUrl += (newUrl.indexOf('?') === -1 ? '?' : '&');\n newUrl += 'dcp=e4988ba0b1c993e062fc7c536188bda3ea4de883,' + Date.now() + hash;\n\n window.location.replace(newUrl);\n }\n}\n\nObject.assign(module.exports, {\n SchedMsgWeb\n});\n\n\n//# sourceURL=webpack:///./src/dcp-client/schedmsg/schedmsg-web.js?");
5203
5203
 
5204
5204
  /***/ }),
5205
5205
 
@@ -5210,7 +5210,7 @@ eval("/**\n * @file /src/schedmsg/schedmsg-web.js\n * @author Ryan Rossi
5210
5210
  /*! no static exports found */
5211
5211
  /***/ (function(module, exports, __webpack_require__) {
5212
5212
 
5213
- eval("/**\n * @file /src/schedmsg/schedmsg.js\n * @author Ryan Rossiter, ryan@kingsds.network\n * @date March 2020\n *\n * This is the client logic for subscribing to and handling SchedMsg events.\n * It inherits from PropagatingEventEmitter, and will register default event\n * callbacks for the commands, but they can be overridden by registering\n * additional event callbacks that return 'false'. This will prevent the default\n * behaviour from occuring.\n */\n\nconst { EventSubscriber } = __webpack_require__(/*! dcp/events/event-subscriber */ \"./src/events/event-subscriber.js\");\nconst { PropagatingEventEmitter } = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\");\nconst protocolV4 = __webpack_require__(/*! dcp/protocol-v4 */ \"./src/protocol-v4/index.js\");\n\n/** @typedef {import('../worker').Worker} Worker */\n\n/**\n * SchedMsg is the class that will subscribe to receive commands from the scheduler.\n * These commands could be used to reload the worker, or broacast a message.\n * \n * SchedMsg extends the PropagatingEventEmitter.\n * When a command is received from the scheduler, it will be emitted on the Schedsg\n * instance. SchedMsg will register default handlers for these commands, but they can\n * be overridden by listening for the event and returning `false` from callback you provide.\n * @access public\n */\nclass SchedMsg extends PropagatingEventEmitter {\n /**\n * This command instructs the worker to immediately stop working, and can optionally disable the worker to prevent restarting. The user will need to manually intervene to restart the worker.\n * \n * @event SchedMsg#kill\n * @access public\n * @type {object}\n * @property {boolean} temporary When false, the worker will be disabled.\n *//**\n * This command instructs the worker to restart, e.g. call `worker.stop()` then `worker.start()`.\n * \n * @event SchedMsg#restart\n * @access public\n *//**\n * This command instructs the worker to stop working on a specific job.\n * \n * @event SchedMsg#remove\n * @access public\n * @type {object}\n * @property {string} jobAddress The address of the job to stop working on.\n *//**\n * This command is an announcement from the scheduler, the provided message should be displayed to the user (modal on web, console on node).\n * \n * @event SchedMsg#announce\n * @access public\n * @type {object}\n * @property {string} message The message to be displayed to the user.\n *//**\n * This command instructs the worker to \"hard\" reload, in the browser this will trigger a page refresh and in node it will exit the process.\n * \n * @event SchedMsg#reload\n * @access public\n *//**\n * This *web-only* command will open a new webpage to the provided URL.\n * \n * @event SchedMsg#openPopup\n * @access public\n * @type {object}\n * @property {string} href The URL to open the new page to.\n */\n\n /**\n * @constructor\n * @param {Worker} worker \n */\n constructor(worker) {\n super('SchedMsg');\n\n this.eventSubscriber = new EventSubscriber();\n\n this.worker = worker;\n this.handlers = {};\n\n this.subscriptionIds = null;\n\n this.registerHandler('kill', (opts) => this.onKill(opts));\n this.registerHandler('restart', (opts) => this.onRestart(opts));\n this.registerHandler('remove', (opts) => this.onRemove(opts));\n this.registerHandler('addPriorityJob', (opts) => this.onAddPriorityJob(opts));\n this.registerHandler('removePriorityJob', (opts) => this.onRemovePriorityJob(opts));\n this.registerHandler('pingWorkerId', (opts) => this.onPingWorkerId(opts));\n }\n\n async start() {\n if (!this.eventSubscriber.eventRouterConnection.state.is('established')) {\n await this.eventSubscriber.eventRouterConnection.connect();\n }\n const workerId = this.worker.supervisor.workerId || this.worker.supervisor.workerOpaqueId;\n const identityAddress = this.eventSubscriber.eventRouterConnection.identity.address;\n this.identityAddress = identityAddress;\n this.workerId = workerId;\n this.subscriptionIds = await Promise.all([\n this.eventSubscriber.subscribe('schedmsg::broadcast', this.onMessage.bind(this)),\n this.eventSubscriber.subscribe('schedmsg::command', this.onMessage.bind(this), {\n filter: {\n $or: [\n {target: identityAddress},\n {target: workerId},\n ],\n },\n })\n ]);\n }\n\n async stop() {\n if (!this.subscriptionIds) {\n // nothing to be done, quietly return\n return;\n }\n\n await Promise.all(this.subscriptionIds.map((subscriptionId) =>\n this.eventSubscriber.unsubscribe(subscriptionId)\n ));\n\n await this.eventSubscriber.close();\n }\n\n registerHandler(command, callback) {\n if (this.listenerCount(command) > 0) {\n throw new Error(`There is a handler already registered for command '${command}'`);\n }\n\n this.on(command, callback);\n }\n\n onMessage(msg) {\n const { command, payload } = msg;\n\n if (this.listenerCount(command) > 0) {\n this.emit(command, payload);\n } else {\n console.warn(`No SchedMsg handler registered for command '${command}'`);\n }\n }\n\n onKill({ temporary }) {\n console.log(\n \"!!!!!!!!!!!!!!!\\n\\n\",\n \"Kill command received from scheduler. Stopping worker...\",\n \"\\n\\n!!!!!!!!!!!!!!!\");\n\n if (this.worker.working) {\n this.worker.stop(true);\n }\n\n if (!temporary) {\n this.worker.constructor.disableWorker();\n }\n }\n\n onRestart() {\n if (this.worker.working) {\n this.worker.stop();\n setTimeout(() => {\n this.worker.start();\n }, Math.random() * 10000 /* stagger workers coming back online */);\n }\n }\n\n onRemove({ jobAddress }) {\n for (let sandbox of this.worker.supervisor.workingSandboxes) {\n if (sandbox.jobAddress === jobAddress) {\n this.worker.supervisor.returnSandbox(sandbox, true);\n }\n }\n }\n\n async onAddPriorityJob({ jobAddress, immediate }) {\n console.log('Received addPriorityJob schedmsg, adding job:', jobAddress);\n\n const supervisor = this.worker.supervisor;\n supervisor.options.jobAddresses.push(jobAddress);\n\n if (immediate) {\n // return all slices that aren't in the priority job list\n await Promise.all(\n supervisor.slices\n .filter((slice) => !supervisor.options.jobAddresses.includes(slice.jobAddress))\n .map((slice) => supervisor.returnSlice(slice))\n ).catch((e) => {\n console.error(\"Error while returning slice before handling addPriorityJob schedmsg:\");\n console.error(e);\n });\n\n // stop all current sandboxes that aren't working on priority slices\n for (let sandbox of supervisor.workingSandboxes) {\n if (!supervisor.options.jobAddresses.includes(sandbox.jobAddress)) {\n supervisor.returnSandbox(sandbox, true);\n }\n }\n }\n\n\n this.eventSubscriber.subscribe('job::stop', () => {\n supervisor.options.jobAddresses =\n supervisor.options.jobAddresses.filter((Address) => Address !== jobAddress);\n }, {\n filter: { job: jobAddress },\n })\n .catch((e) => {\n console.error(\"Failed to subscribe to job::stop event for addPriorityJob schedmsg:\");\n console.error(e);\n });\n }\n\n async onRemovePriorityJob({ jobAddress, immediate }) {\n console.log('Received removePriorityJob schedmsg, removing job:', jobAddress);\n\n const supervisor = this.worker.supervisor;\n supervisor.options.jobAddresses =\n supervisor.options.jobAddresses.filter((address) => address !== jobAddress);\n\n if (immediate) {\n // return all slices belonging to this job\n await Promise.all(\n supervisor.slices\n .filter((slice) => slice.jobAddress === jobAddress)\n .map((slice) => supervisor.returnSlice(slice))\n ).catch((e) => {\n console.error(\"Error while returning slice for immediately removed job:\");\n console.error(e);\n });\n\n // stop all current sandboxes that are working on this job\n for (let sandbox of supervisor.workingSandboxes) {\n if (sandbox.jobAddress === jobAddress) {\n supervisor.returnSandbox(sandbox, true);\n }\n }\n }\n }\n async onPingWorkerId() {\n const workerIdentity = this.eventSubscriber.eventRouterConnection.identity;\n let thanatosConnection;\n try {\n thanatosConnection = new protocolV4.Connection(dcpConfig.scheduler.services.thanatos.location, workerIdentity);\n await thanatosConnection.send('worker-pong', {\n workerId: this.workerId,\n });\n } finally {\n await thanatosConnection.close();\n }\n }\n}\n\nexports.SchedMsg = SchedMsg;\n\n\n//# sourceURL=webpack:///./src/dcp-client/schedmsg/schedmsg.js?");
5213
+ eval("/**\n * @file /src/schedmsg/schedmsg.js\n * @author Ryan Rossiter, ryan@kingsds.network\n * @date March 2020\n *\n * This is the client logic for subscribing to and handling SchedMsg events.\n * It inherits from PropagatingEventEmitter, and will register default event\n * callbacks for the commands, but they can be overridden by registering\n * additional event callbacks that return 'false'. This will prevent the default\n * behaviour from occuring.\n */\n\nconst { EventSubscriber } = __webpack_require__(/*! dcp/events/event-subscriber */ \"./src/events/event-subscriber.js\");\nconst { PropagatingEventEmitter } = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\");\nconst protocolV4 = __webpack_require__(/*! dcp/protocol-v4 */ \"./src/protocol-v4/index.js\");\n\n/** @typedef {import('../worker').Worker} Worker */\n\n/**\n * SchedMsg is the class that will subscribe to receive commands from the scheduler.\n * These commands could be used to reload the worker, or broacast a message.\n * \n * SchedMsg extends the PropagatingEventEmitter.\n * When a command is received from the scheduler, it will be emitted on the Schedsg\n * instance. SchedMsg will register default handlers for these commands, but they can\n * be overridden by listening for the event and returning `false` from callback you provide.\n * @access public\n */\nclass SchedMsg extends PropagatingEventEmitter {\n /**\n * This command instructs the worker to immediately stop working, and can optionally disable the worker to prevent restarting. The user will need to manually intervene to restart the worker.\n * \n * @event SchedMsg#kill\n * @access public\n * @type {object}\n * @property {boolean} temporary When false, the worker will be disabled.\n *//**\n * This command instructs the worker to restart, e.g. call `worker.stop()` then `worker.start()`.\n * \n * @event SchedMsg#restart\n * @access public\n *//**\n * This command instructs the worker to stop working on a specific job.\n * \n * @event SchedMsg#remove\n * @access public\n * @type {object}\n * @property {string} jobAddress The address of the job to stop working on.\n *//**\n * This command is an announcement from the scheduler, the provided message should be displayed to the user (modal on web, console on node).\n * \n * @event SchedMsg#announce\n * @access public\n * @type {object}\n * @property {string} message The message to be displayed to the user.\n *//**\n * This command instructs the worker to \"hard\" reload, in the browser this will trigger a page refresh and in node it will exit the process.\n * \n * @event SchedMsg#reload\n * @access public\n *//**\n * This *web-only* command will open a new webpage to the provided URL.\n * \n * @event SchedMsg#openPopup\n * @access public\n * @type {object}\n * @property {string} href The URL to open the new page to.\n */\n\n /**\n * @constructor\n * @param {Worker} worker \n */\n constructor(worker) {\n super('SchedMsg');\n\n this.eventSubscriber = new EventSubscriber(this);\n\n this.worker = worker;\n this.handlers = {};\n\n this.registerHandler('kill', (opts) => this.onKill(opts));\n this.registerHandler('restart', (opts) => this.onRestart(opts));\n this.registerHandler('remove', (opts) => this.onRemove(opts));\n this.registerHandler('addPriorityJob', (opts) => this.onAddPriorityJob(opts));\n this.registerHandler('removePriorityJob', (opts) => this.onRemovePriorityJob(opts));\n this.registerHandler('pingWorkerId', (opts) => this.onPingWorkerId(opts));\n }\n\n async start() {\n if (!this.eventSubscriber.eventRouterConnection.state.is('established')) {\n await this.eventSubscriber.eventRouterConnection.connect();\n }\n const workerId = this.worker.supervisor.workerId || this.worker.supervisor.workerOpaqueId;\n const identityAddress = this.eventSubscriber.eventRouterConnection.identity.address;\n this.identityAddress = identityAddress;\n this.workerId = workerId;\n this.eventIntercepts = {}\n this.eventIntercepts['schedmsg::broadcast'] = this.onMessage.bind(this);\n this.eventIntercepts['schedmsg::command'] = this.onMessage.bind(this);\n\n await this.eventSubscriber.subscribe('schedmsg::broadcast', {\n filter: {\n $or: [\n {target: identityAddress},\n {target: workerId},\n ],\n },\n });\n await this.eventSubscriber.subscribe('schedmsg::command', {\n filter: {\n $or: [\n {target: identityAddress},\n {target: workerId},\n ],\n },\n });\n }\n\n async stop() {\n this.eventSubscriber.unsubscribe('schedmsg::broadcast')\n this.eventSubscriber.unsubscribe('schedmsg::command')\n\n await this.eventSubscriber.close();\n }\n\n registerHandler(command, callback) {\n if (this.listenerCount(command) > 0) {\n throw new Error(`There is a handler already registered for command '${command}'`);\n }\n\n this.on(command, callback);\n }\n\n onMessage(msg) {\n const { command, payload } = msg;\n\n if (this.listenerCount(command) > 0) {\n this.emit(command, payload);\n } else {\n console.warn(`No SchedMsg handler registered for command '${command}'`);\n }\n }\n\n onKill({ temporary }) {\n console.log(\n \"!!!!!!!!!!!!!!!\\n\\n\",\n \"Kill command received from scheduler. Stopping worker...\",\n \"\\n\\n!!!!!!!!!!!!!!!\");\n\n if (this.worker.working) {\n this.worker.stop(true);\n }\n\n if (!temporary) {\n this.worker.constructor.disableWorker();\n }\n }\n\n onRestart() {\n if (this.worker.working) {\n this.worker.stop();\n setTimeout(() => {\n this.worker.start();\n }, Math.random() * 10000 /* stagger workers coming back online */);\n }\n }\n\n onRemove({ jobAddress }) {\n for (let sandbox of this.worker.supervisor.workingSandboxes) {\n if (sandbox.jobAddress === jobAddress) {\n this.worker.supervisor.returnSandbox(sandbox, true);\n }\n }\n }\n\n async onAddPriorityJob({ jobAddress, immediate }) {\n console.log('Received addPriorityJob schedmsg, adding job:', jobAddress);\n\n const supervisor = this.worker.supervisor;\n supervisor.options.jobAddresses.push(jobAddress);\n\n if (immediate) {\n // return all slices that aren't in the priority job list\n await Promise.all(\n supervisor.slices\n .filter((slice) => !supervisor.options.jobAddresses.includes(slice.jobAddress))\n .map((slice) => supervisor.returnSlice(slice))\n ).catch((e) => {\n console.error(\"Error while returning slice before handling addPriorityJob schedmsg:\");\n console.error(e);\n });\n\n // stop all current sandboxes that aren't working on priority slices\n for (let sandbox of supervisor.workingSandboxes) {\n if (!supervisor.options.jobAddresses.includes(sandbox.jobAddress)) {\n supervisor.returnSandbox(sandbox, true);\n }\n }\n }\n\n this.eventIntercepts['stop'] = () => {\n supervisor.options.jobAddresses =\n supervisor.options.jobAddresses.filter((Address) => Address !== jobAddress);\n }\n this.eventSubscriber.subscribe('job::optionalEvent', {\n filter: { job: jobAddress , eventName: 'stop' },\n })\n .catch((e) => {\n console.error(\"Failed to subscribe to stop event for addPriorityJob schedmsg:\");\n console.error(e);\n });\n }\n\n async onRemovePriorityJob({ jobAddress, immediate }) {\n console.log('Received removePriorityJob schedmsg, removing job:', jobAddress);\n\n const supervisor = this.worker.supervisor;\n supervisor.options.jobAddresses =\n supervisor.options.jobAddresses.filter((address) => address !== jobAddress);\n\n if (immediate) {\n // return all slices belonging to this job\n await Promise.all(\n supervisor.slices\n .filter((slice) => slice.jobAddress === jobAddress)\n .map((slice) => supervisor.returnSlice(slice))\n ).catch((e) => {\n console.error(\"Error while returning slice for immediately removed job:\");\n console.error(e);\n });\n\n // stop all current sandboxes that are working on this job\n for (let sandbox of supervisor.workingSandboxes) {\n if (sandbox.jobAddress === jobAddress) {\n supervisor.returnSandbox(sandbox, true);\n }\n }\n }\n }\n async onPingWorkerId() {\n const workerIdentity = this.eventSubscriber.eventRouterConnection.identity;\n let thanatosConnection;\n try {\n thanatosConnection = new protocolV4.Connection(dcpConfig.scheduler.services.thanatos.location, workerIdentity);\n await thanatosConnection.send('worker-pong', {\n workerId: this.workerId,\n });\n } finally {\n await thanatosConnection.close();\n }\n }\n}\n\nexports.SchedMsg = SchedMsg;\n\n\n//# sourceURL=webpack:///./src/dcp-client/schedmsg/schedmsg.js?");
5214
5214
 
5215
5215
  /***/ }),
5216
5216
 
@@ -5380,7 +5380,7 @@ eval("// NOTE - need timeout/postmessage function\n/**\n * @file dcp-client/work
5380
5380
  /***/ (function(module, exports, __webpack_require__) {
5381
5381
 
5382
5382
  "use strict";
5383
- eval("/**\n * @file worker/slice.js\n *\n * A wrapper for the slice object returned from the scheduler.\n * It has useful helper functions such as @getNextSlice and @getResultsMessage\n *\n *\n * @author Matthew Palma, mpalma@kingsds.network\n * Ryan Rossiter, ryan@kingsds.network\n * @date May 2019\n */\n\n/* global dcpConfig, protocol */\n\n\nconst SLICE_STATUS_UNASSIGNED = 'UNASSIGNED';\nconst SLICE_STATUS_WORKING = 'WORKING';\nconst SLICE_STATUS_COMPLETE = 'COMPLETE';\nconst SLICE_STATUS_FAILED = 'FAILED';\n\n/** \n * Object use to represent a given slice inside the Supervisor. This object's shape \n * current inherits heavily from the message payload originating from the scheduler,\n * but should not /wg dec 2020.\n *\n * Caveat lector: this documentation was created without a 100% understanding of the\n * code. Please improve as possible.\n *\n * The properties of this object are as follows:\n * - sliceNumber the number of the slice within this job\n * - jobAddress the address of the job that this slice belongs to\n * - datumUri the URI describing the data (input set element) for \n * this slice of the job; could be a data: URI or it\n * could be a URL we need to fetch; note that fetches \n * are limited to worker's allow list\n * - resultStorageType 'values' => we are storing individual values,\n * which could be data: URIs or URLs\n * at the scheduler\n * 'pattern' => user-specified pattern for result URLs.\n * Data will be uploaded via POST to the\n * URL matching the pattern, provided the\n * worker is allowed to access it.\n * - resultStorageParams user-supplied POST parameters sent to the result \n * storage server when using resultStorageType = pattern.\n * - resultStorageDetails the pattern when using resultStorageType = pattern.\n * - result\n * - result.request 'complete' | ???\n * - result.result return value from work function\n * - result.timeReport { total, idle } ms \n *\n * - status COMPLETE | FAILED | WORKING | UNASSIGNED\n */\nclass Slice {\n constructor (taskElement) {\n /** @deprecated - UNUSED */\n this.index = taskElement.sliceNumber; /* deprecated - do not use */\n /** @type {string} */\n this.status = SLICE_STATUS_UNASSIGNED;\n /** @type {boolean} */\n this.isEstimationSlice = false;\n /** @type {boolean} */\n this.isLongSlice = taskElement.isLongSlice;\n /** @type {number} */\n this.sliceNumber = taskElement.sliceNumber;\n this.jobAddress = taskElement.jobAddress;\n this.datumUri = taskElement.datumUri;\n this.resultStorageParams = taskElement.resultStorageParams;\n this.resultStorageDetails = taskElement.resultStorageDetails;\n\n this.result = null;\n this.error = null;\n }\n\n get identifier() {\n return `${this.jobAddress}-${this.sliceNumber}`;\n }\n\n /**\n * @returns {boolean} - true if the slice is ready to be submitted \n */\n get isFinished() {\n return this.status === SLICE_STATUS_COMPLETE || this.status === SLICE_STATUS_FAILED;\n }\n\n get failed () {\n return this.status === SLICE_STATUS_FAILED;\n }\n\n get completed () {\n return this.status === SLICE_STATUS_COMPLETE;\n }\n\n get isUnassigned () {\n return this.status === SLICE_STATUS_UNASSIGNED\n }\n\n /**\n * @returns {boolean} - true if all slices have been handed out, does not imply they are complete\n */\n get isWorking () {\n return this.status === SLICE_STATUS_WORKING;\n }\n\n // Sets the slice status to working, called when the slice is handed to a worker\n markAsWorking() {\n if (!this.isUnassigned) throw new Error(`Slice ${this.identifier} is already assigned, can't mark as working. status = ${this.status}`);\n this.status = SLICE_STATUS_WORKING;\n }\n\n /**\n * Receives a result from the scheduler.\n * It will then put the result in the appropriate place.\n * It could also eventually determine if the slice should be\n * retried before determining that it has failed.\n *\n * @param {object|Error} result - the result that came back from the worker\n * @param {boolean} [success = true] - true is result is considered successful, false if something went wrong computing\n */\n collectResult (result, success = true) {\n if (this.result)\n throw new Error(`Slice ${this.identifier} received more than one result`);\n \n if (success) {\n this.status = SLICE_STATUS_COMPLETE;\n this.result = result;\n } else {\n this.status = SLICE_STATUS_FAILED;\n this.error = result;\n }\n }\n\n /**\n * Create and return the payload object to send back to the result submitter\n */\n getResultMessagePayload () {\n if (!this.hasOwnProperty('result'))\n throw new Error(`Cannot create result message payload for slice ${this.jobAddress}::${this.sliceNumber}`);\n\n return {\n job: this.jobAddress,\n slice: this.sliceNumber,\n result: this.result,\n }; \n }\n\n /**\n * Create and return a slice-return payload to send to the\n * result-submitter's status operation\n *\n * @param {address} worker The current worker's opaque\n * @param {object} authorizationMessage\n * @param {string} [reason] Optional reason for the return:\n * noprogress, uncaught, missingDepends, etc.\n *\n * @return {object} Message payload; still requires worker\n * address \n */\n getReturnMessagePayload (worker, authorizationMessage, reason) {\n delete this.result;\n\n return {\n worker,\n slices: [{\n job: this.jobAddress,\n sliceNumber: this.sliceNumber,\n status: 'return',\n isEstimationSlice: this.isEstimationSlice,\n authorizationMessage,\n reason,\n error:this.error\n }],\n };\n }\n}\n\nexports.Slice = Slice;\nexports.SLICE_STATUS_UNASSIGNED = SLICE_STATUS_UNASSIGNED;\nexports.SLICE_STATUS_WORKING = SLICE_STATUS_WORKING;\nexports.SLICE_STATUS_COMPLETE = SLICE_STATUS_COMPLETE;\nexports.SLICE_STATUS_FAILED = SLICE_STATUS_FAILED;\n\n\n\n//# sourceURL=webpack:///./src/dcp-client/worker/slice.js?");
5383
+ eval("/**\n * @file worker/slice.js\n *\n * A wrapper for the slice object returned from the scheduler.\n * It has useful helper functions such as @getNextSlice and @getResultsMessage\n *\n *\n * @author Matthew Palma, mpalma@kingsds.network\n * Ryan Rossiter, ryan@kingsds.network\n * @date May 2019\n */\n\n/* global dcpConfig, protocol */\n\n\nconst SLICE_STATUS_UNASSIGNED = 'UNASSIGNED';\nconst SLICE_STATUS_WORKING = 'WORKING';\nconst SLICE_STATUS_COMPLETE = 'COMPLETE';\nconst SLICE_STATUS_FAILED = 'FAILED';\n\n/** \n * Object use to represent a given slice inside the Supervisor. This object's shape \n * current inherits heavily from the message payload originating from the scheduler,\n * but should not /wg dec 2020.\n *\n * Caveat lector: this documentation was created without a 100% understanding of the\n * code. Please improve as possible.\n *\n * The properties of this object are as follows:\n * - sliceNumber the number of the slice within this job\n * - jobAddress the address of the job that this slice belongs to\n * - datumUri the URI describing the data (input set element) for \n * this slice of the job; could be a data: URI or it\n * could be a URL we need to fetch; note that fetches \n * are limited to worker's allow list\n * - resultStorageType 'values' => we are storing individual values,\n * which could be data: URIs or URLs\n * at the scheduler\n * 'pattern' => user-specified pattern for result URLs.\n * Data will be uploaded via POST to the\n * URL matching the pattern, provided the\n * worker is allowed to access it.\n * - resultStorageParams user-supplied POST parameters sent to the result \n * storage server when using resultStorageType = pattern.\n * - resultStorageDetails the pattern when using resultStorageType = pattern.\n * - result\n * - result.request 'complete' | ???\n * - result.result return value from work function\n * - result.timeReport { total, idle } ms \n *\n * - status COMPLETE | FAILED | WORKING | UNASSIGNED\n */\nclass Slice {\n constructor (taskElement) {\n /** @deprecated - UNUSED */\n this.index = taskElement.sliceNumber; /* deprecated - do not use */\n /** @type {string} */\n this.status = SLICE_STATUS_UNASSIGNED;\n /** @type {boolean} */\n this.isEstimationSlice = false;\n /** @type {boolean} */\n this.isLongSlice = taskElement.isLongSlice;\n /** @type {number} */\n this.sliceNumber = taskElement.sliceNumber;\n this.jobAddress = taskElement.jobAddress;\n this.datumUri = taskElement.datumUri;\n this.resultStorageType = taskElement.resultStorageType;\n this.resultStorageParams = taskElement.resultStorageParams;\n this.resultStorageDetails = taskElement.resultStorageDetails;\n\n this.result = null;\n this.error = undefined;\n }\n\n get identifier() {\n return `${this.jobAddress}-${this.sliceNumber}`;\n }\n\n /**\n * @returns {boolean} - true if the slice is ready to be submitted \n */\n get isFinished() {\n return this.status === SLICE_STATUS_COMPLETE || this.status === SLICE_STATUS_FAILED;\n }\n\n get failed () {\n return this.status === SLICE_STATUS_FAILED;\n }\n\n get completed () {\n return this.status === SLICE_STATUS_COMPLETE;\n }\n\n get isUnassigned () {\n return this.status === SLICE_STATUS_UNASSIGNED\n }\n\n /**\n * @returns {boolean} - true if all slices have been handed out, does not imply they are complete\n */\n get isWorking () {\n return this.status === SLICE_STATUS_WORKING;\n }\n\n // Sets the slice status to working, called when the slice is handed to a worker\n markAsWorking() {\n if (!this.isUnassigned) throw new Error(`Slice ${this.identifier} is already assigned, can't mark as working. status = ${this.status}`);\n this.status = SLICE_STATUS_WORKING;\n }\n\n /**\n * Receives a result from the scheduler.\n * It will then put the result in the appropriate place.\n * It could also eventually determine if the slice should be\n * retried before determining that it has failed.\n *\n * @param {object|Error} result - the result that came back from the worker\n * @param {boolean} [success = true] - true is result is considered successful, false if something went wrong computing\n */\n collectResult (result, success = true) {\n if (this.result)\n throw new Error(`Slice ${this.identifier} received more than one result`);\n \n if (success) {\n this.status = SLICE_STATUS_COMPLETE;\n this.result = result;\n } else {\n this.status = SLICE_STATUS_FAILED;\n this.error = result;\n }\n }\n\n /**\n * Create and return the payload object to send back to the result submitter\n */\n getResultMessagePayload () {\n if (!this.hasOwnProperty('result'))\n throw new Error(`Cannot create result message payload for slice ${this.jobAddress}::${this.sliceNumber}`);\n\n return {\n job: this.jobAddress,\n slice: this.sliceNumber,\n result: this.result,\n }; \n }\n\n /**\n * Create and return a slice-return payload to send to the\n * result-submitter's status operation\n *\n * @param {address} worker The current worker's opaque\n * @param {object} authorizationMessage\n * @param {string} [reason] Optional reason for the return:\n * noprogress, uncaught, missingDepends, etc.\n *\n * @return {object} Message payload; still requires worker\n * address \n */\n getReturnMessagePayload (worker, authorizationMessage, reason) {\n delete this.result;\n\n return {\n worker,\n slices: [{\n job: this.jobAddress,\n sliceNumber: this.sliceNumber,\n status: 'return',\n isEstimationSlice: this.isEstimationSlice,\n authorizationMessage,\n reason,\n error:this.error\n }],\n };\n }\n}\n\nexports.Slice = Slice;\nexports.SLICE_STATUS_UNASSIGNED = SLICE_STATUS_UNASSIGNED;\nexports.SLICE_STATUS_WORKING = SLICE_STATUS_WORKING;\nexports.SLICE_STATUS_COMPLETE = SLICE_STATUS_COMPLETE;\nexports.SLICE_STATUS_FAILED = SLICE_STATUS_FAILED;\n\n\n//# sourceURL=webpack:///./src/dcp-client/worker/slice.js?");
5384
5384
 
5385
5385
  /***/ }),
5386
5386
 
@@ -5391,7 +5391,7 @@ eval("/**\n * @file worker/slice.js\n *\n * A wrapper for the slice object retur
5391
5391
  /*! no static exports found */
5392
5392
  /***/ (function(module, exports, __webpack_require__) {
5393
5393
 
5394
- eval("/**\n * @file worker/supervisor-cache.js\n *\n * A cache for the supervisor, anything the supervisor\n * may request that is cacheable (the same every time its requested)\n * can be cached in this class.\n *\n * Currently only jobs and modules are cached.\n *\n * If a cached job is not accessed in tuning.jobCacheTTL ms\n * it will be invalidated.\n *\n * @author Matthew Palma, mpalma@kingsds.network\n * @date May 2019\n */\n\n/* global dcpConfig */\n\nconst debugging = __webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope('worker');\nconst { justFetch, fetchURI, dumpObject } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\nconst { EventEmitter } = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\");\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\n\nconst tuning = dcpConfig.future('supervisor.tuning', {\n jobCacheTTL: 300, /* seconds */\n jobCacheCleanupInterval: 60, /* seconds */\n});\n\nclass SupervisorCache extends EventEmitter {\n constructor (supervisor) {\n var timer;\n \n super('SupervisorCache')\n this.supervisor = supervisor;\n this.cache = {\n job: {},\n module: {},\n dependency: {},\n }\n \n this.promises = {\n job: {},\n module: {},\n dependency: {},\n }\n\n this.lastAccess = {}\n for (let key in this.cache) {\n this.lastAccess[key] = {}\n }\n\n timer = setInterval(this.checkTTL, tuning.jobCacheCleanupInterval);\n if (timer.unref)\n timer.unref();\n }\n\n /**\n * Returns an object listing what jobs and modules are currently cached.\n * @returns {object} - in the form: { job: [0xgen1, 0xgen2,...], modules: [modGroup1, modGroup2,...] }\n */\n get cacheDescription () {\n let description = {}\n for (let key in this.cache) {\n description[key] = Object.keys(this.cache[key])\n }\n return description\n }\n\n /**\n * Returns an array of all jobId currently cached\n * @returns {Array} all the jobId's in the cache\n */\n get jobs () {\n return Object.keys(this.cache.job);\n }\n\n /**\n * Runs on an interval and checks to see if a cached object \n * should be invalidated because it hasn't been accessed recently.\n */\n checkTTL () {\n for (let key in this.lastAccess) {\n for (let address in this.lastAccess[key]) {\n let lastAccess = this.lastAccess[key][address]\n let now = Date.now()\n if (lastAccess + (1000 * tuning.jobCacheTTL) < now) { /* dcpConfig.supervisor.cacheTTL */\n delete this.cache[key][address]\n }\n }\n }\n }\n\n /**\n * Attempts to look up an item from the cache.\n * If item is found its last access time is updated.\n *\n * @param {string} key - the cache to look in (job or module)\n * @param {string} id - the items identifier (jobAddress or module group name)\n *\n * @returns {any} The value of the stored item or null if nothing is found\n */\n fetch(key, id) {\n if (typeof this.cache[key] === 'undefined') {\n throw new Error(`${key} does not relate to any cache.`);\n }\n if (this.cache[key][id]) {\n this.lastAccess[key][id] = Date.now();\n return this.cache[key][id];\n }\n return null;\n }\n\n /**\n * Stores a fetched value for one of the caches.\n *\n * @param {string} key - the cache to store the item in\n * @param {string} id - the items identifier (job Address or module group name)\n * @param {any} value - the item to store\n *\n * @returns {any} - @value passed in\n */\n store (key, id, value) {\n if (typeof this.cache[key] === 'undefined') {\n throw new Error(`${key} does not relate to any cache.`)\n }\n this.cache[key][id] = value\n this.lastAccess[key][id] = Date.now()\n return value\n }\n\n /** \n * Fetch a job from the job cache. If the job has components which are \n * which need to be fetched over the network, they are fetched before the\n * returned promise is resolved.\n *\n * The job cache is initially populated during fetchTask.\n */\n async fetchJob(address, allowedOrigins) {\n let job = this.fetch('job', address);\n\n if (!job) {\n let e = new Error(`No job in supervisor cache with address ${address}`);\n e.code = 'ENOJOB';\n throw e;\n }\n\n // XXXpfr Excellent tracing.\n if (debugging('worker')) {\n dumpObject(job, 'SupervisorCache.fetchJob: job', 128);\n }\n\n if (!job.workFunction) {\n job.workFunction = await fetchURI(job.codeLocation, allowedOrigins, dcpConfig.worker.allowOrigins.fetchWorkFunctions);\n delete job.codeLocation;\n }\n if (!job.arguments) {\n job.arguments = await fetchURI(job.argumentsLocation, dcpConfig.worker.allowOrigins.any, dcpConfig.worker.allowOrigins.fetchArguments);\n delete job.argumentsLocation;\n }\n \n return job;\n }\n\n /**\n * Attempts to fetch a module group from the cache and\n * if it's not found it attempts to fetch then store\n * the module group from the package manager.\n *\n * @param {array} modulesArray - the array of modules requested \n * - (when stringified it's the identifier of the module group)\n *\n * @returns {Promise<object>} - the module group\n * @throws when the module group can not be fetched\n */\n async fetchModule(modulesArray) {\n const cacheKey = JSON.stringify(modulesArray);\n let modules = this.fetch('module', cacheKey);\n if (modules !== null) {\n return modules;\n }\n\n if (this.promises.module[cacheKey]) {\n return this.promises.module[cacheKey];\n }\n\n const {\n success,\n payload: responsePayload,\n } = await this.supervisor.packageManagerConnection.send('fetchModule', {\n modules: modulesArray,\n });\n\n if (!success) {\n /**\n * Preserving the error message by not rewrapping it with DCPError incase\n * we want to let clients know which module couldn't be fetched.\n */\n throw responsePayload;\n }\n\n this.promises.module[cacheKey] = responsePayload;\n modules = await this.promises.module[cacheKey];\n delete this.promises.module[cacheKey];\n return this.store('module', cacheKey, modules);\n }\n\n /**\n * Attempts to fetch a dependency from the cache and\n * if it's not found it attempts to fetch then store\n * the dependency from the package manager.\n *\n * @param {string} dependencyUri - The URI of the dependency\n *\n * @returns {Promise<string>} file contents\n * @throws when the dependency can not be fetched\n */\n async fetchDependency(dependencyUri) {\n let dependency = this.fetch('dependency', dependencyUri);\n if (dependency !== null) {\n return dependency;\n }\n\n if (this.promises.dependency[dependencyUri]) {\n return this.promises.dependency[dependencyUri];\n }\n\n const url = dcpConfig.packageManager.location.resolve(dependencyUri);\n this.promises.dependency[dependencyUri] = justFetch(url, 'string', 'GET', true);\n\n dependency = await this.promises.dependency[dependencyUri];\n\n delete this.promises.dependency[dependencyUri];\n\n return this.store('dependency', dependencyUri, dependency);\n }\n}\n\nexports.SupervisorCache = SupervisorCache;\n\n\n//# sourceURL=webpack:///./src/dcp-client/worker/supervisor-cache.js?");
5394
+ eval("/**\n * @file worker/supervisor-cache.js\n *\n * A cache for the supervisor, anything the supervisor\n * may request that is cacheable (the same every time its requested)\n * can be cached in this class.\n *\n * Currently only jobs and modules are cached.\n *\n * If a cached job is not accessed in tuning.jobCacheTTL ms\n * it will be invalidated.\n *\n * @author Matthew Palma, mpalma@kingsds.network\n * @date May 2019\n */\n\n/* global dcpConfig */\n\nconst debugging = __webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope('worker');\nconst { justFetch, fetchURI, dumpObject } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\nconst { EventEmitter } = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\");\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\n\nconst tuning = dcpConfig.future('supervisor.tuning', {\n jobCacheTTL: 300, /* seconds */\n jobCacheCleanupInterval: 60, /* seconds */\n});\n\nclass SupervisorCache extends EventEmitter {\n constructor (supervisor) {\n var timer;\n \n super('SupervisorCache')\n this.supervisor = supervisor;\n this.cache = {\n job: {},\n module: {},\n dependency: {},\n }\n \n this.promises = {\n job: {},\n module: {},\n dependency: {},\n }\n\n this.lastAccess = {}\n for (let key in this.cache) {\n this.lastAccess[key] = {}\n }\n\n timer = setInterval(this.checkTTL, tuning.jobCacheCleanupInterval);\n if (timer.unref)\n timer.unref();\n }\n\n /**\n * Returns an object listing what jobs and modules are currently cached.\n * @returns {object} - in the form: { job: [0xgen1, 0xgen2,...], modules: [modGroup1, modGroup2,...] }\n */\n get cacheDescription () {\n let description = {}\n for (let key in this.cache) {\n description[key] = Object.keys(this.cache[key])\n }\n return description\n }\n\n /**\n * Returns an array of all jobId currently cached\n * @returns {Array} all the jobId's in the cache\n */\n get jobs () {\n return Object.keys(this.cache.job);\n }\n\n /**\n * Runs on an interval and checks to see if a cached object \n * should be invalidated because it hasn't been accessed recently.\n */\n checkTTL () {\n for (let key in this.lastAccess) {\n for (let address in this.lastAccess[key]) {\n let lastAccess = this.lastAccess[key][address]\n let now = Date.now()\n if (lastAccess + (1000 * tuning.jobCacheTTL) < now) { /* dcpConfig.supervisor.cacheTTL */\n delete this.cache[key][address]\n }\n }\n }\n }\n\n /**\n * Attempts to look up an item from the cache.\n * If item is found its last access time is updated.\n *\n * @param {string} key - the cache to look in (job or module)\n * @param {string} id - the items identifier (jobAddress or module group name)\n *\n * @returns {any} The value of the stored item or null if nothing is found\n */\n fetch(key, id) {\n if (typeof this.cache[key] === 'undefined') {\n throw new Error(`${key} does not relate to any cache.`);\n }\n if (this.cache[key][id]) {\n this.lastAccess[key][id] = Date.now();\n return this.cache[key][id];\n }\n return null;\n }\n\n /**\n * Stores a fetched value for one of the caches.\n *\n * @param {string} key - the cache to store the item in\n * @param {string} id - the items identifier (job Address or module group name)\n * @param {any} value - the item to store\n *\n * @returns {any} - @value passed in\n */\n store (key, id, value) {\n if (typeof this.cache[key] === 'undefined') {\n throw new Error(`${key} does not relate to any cache.`)\n }\n this.cache[key][id] = value\n this.lastAccess[key][id] = Date.now()\n return value\n }\n\n /** \n * Fetch a job from the job cache. If the job has components which are \n * which need to be fetched over the network, they are fetched before the\n * returned promise is resolved.\n *\n * The job cache is initially populated during fetchTask.\n */\n async fetchJob(address, allowedOrigins) {\n let job = this.fetch('job', address);\n\n if (!job) {\n let e = new Error(`No job in supervisor cache with address ${address}`);\n e.code = 'ENOJOB';\n throw e;\n }\n\n // XXXpfr Excellent tracing.\n if (debugging('worker')) {\n dumpObject(job, 'SupervisorCache.fetchJob: job', 128);\n }\n\n if (!job.workFunction) {\n job.workFunction = await fetchURI(job.codeLocation, allowedOrigins, dcpConfig.worker.allowOrigins.fetchWorkFunctions);\n delete job.codeLocation;\n }\n if (!job.arguments) {\n if (job.argumentsLocation.startsWith('RemoteDataSet:')){\n let promises = [];\n job.argumentsLocation = job.argumentsLocation.replace('RemoteDataSet:','');\n let uris = job.argumentsLocation.split(\",\");\n for (let i = 0; i < uris.length; i++) {\n promises.push(fetchURI(uris[i], allowedOrigins, dcpConfig.worker.allowOrigins.fetchArguments));\n }\n job.arguments = await Promise.all(promises);\n } else {\n job.arguments = await fetchURI(job.argumentsLocation, allowedOrigins, dcpConfig.worker.allowOrigins.fetchArguments);\n }\n delete job.argumentsLocation;\n }\n \n return job;\n }\n\n /**\n * Attempts to fetch a module group from the cache and\n * if it's not found it attempts to fetch then store\n * the module group from the package manager.\n *\n * @param {array} modulesArray - the array of modules requested \n * - (when stringified it's the identifier of the module group)\n *\n * @returns {Promise<object>} - the module group\n * @throws when the module group can not be fetched\n */\n async fetchModule(modulesArray) {\n const cacheKey = JSON.stringify(modulesArray);\n let modules = this.fetch('module', cacheKey);\n if (modules !== null) {\n return modules;\n }\n\n if (this.promises.module[cacheKey]) {\n return this.promises.module[cacheKey];\n }\n\n const {\n success,\n payload: responsePayload,\n } = await this.supervisor.packageManagerConnection.send('fetchModule', {\n modules: modulesArray,\n });\n\n if (!success) {\n /**\n * Preserving the error message by not rewrapping it with DCPError incase\n * we want to let clients know which module couldn't be fetched.\n */\n throw responsePayload;\n }\n\n this.promises.module[cacheKey] = responsePayload;\n modules = await this.promises.module[cacheKey];\n delete this.promises.module[cacheKey];\n return this.store('module', cacheKey, modules);\n }\n\n /**\n * Attempts to fetch a dependency from the cache and\n * if it's not found it attempts to fetch then store\n * the dependency from the package manager.\n *\n * @param {string} dependencyUri - The URI of the dependency\n *\n * @returns {Promise<string>} file contents\n * @throws when the dependency can not be fetched\n */\n async fetchDependency(dependencyUri) {\n let dependency = this.fetch('dependency', dependencyUri);\n if (dependency !== null) {\n return dependency;\n }\n\n if (this.promises.dependency[dependencyUri]) {\n return this.promises.dependency[dependencyUri];\n }\n\n const url = dcpConfig.packageManager.location.resolve(dependencyUri);\n this.promises.dependency[dependencyUri] = justFetch(url, 'string', 'GET', true);\n\n dependency = await this.promises.dependency[dependencyUri];\n\n delete this.promises.dependency[dependencyUri];\n\n return this.store('dependency', dependencyUri, dependency);\n }\n}\n\nexports.SupervisorCache = SupervisorCache;\n\n\n//# sourceURL=webpack:///./src/dcp-client/worker/supervisor-cache.js?");
5395
5395
 
5396
5396
  /***/ }),
5397
5397
 
@@ -5403,7 +5403,7 @@ eval("/**\n * @file worker/supervisor-cache.js\n *\n * A cache for the superviso
5403
5403
  /***/ (function(module, exports, __webpack_require__) {
5404
5404
 
5405
5405
  "use strict";
5406
- eval("/**\n * @file worker/supervisor.js\n *\n * The component that controls each of the sandboxes\n * and distributes work to them. Also communicates with the\n * scheduler to fetch said work.\n *\n * The supervisor readies sandboxes before/while fetching slices.\n * This means sometimes there are extra instantiated WebWorkers\n * that are idle (in this.readiedSandboxes). Readied sandboxes can\n * be used for any slice. After a readied sandbox is given a slice\n * it becomes assigned to slice's job and can only do work\n * for that job.\n *\n * After a sandbox completes its work, the sandbox becomes cached\n * and can be reused if another slice with a matching job is fetched.\n *\n * @author Matthew Palma, mpalma@kingsds.network\n * Ryan Rossiter, ryan@kingsds.network\n * @date May 2019\n */\n\n/* global dcpConfig */\n// @ts-check\n\n\nconst nanoid = __webpack_require__(/*! nanoid/non-secure */ \"./node_modules/nanoid/non-secure/index.js\");\nconst constants = __webpack_require__(/*! dcp/common/scheduler-constants */ \"./src/common/scheduler-constants.js\");\nconst hash = __webpack_require__(/*! dcp/common/hash */ \"./src/common/hash.js\");\nconst wallet = __webpack_require__(/*! dcp/dcp-client/wallet */ \"./src/dcp-client/wallet/index.js\");\nconst protocolV4 = __webpack_require__(/*! dcp/protocol-v4 */ \"./src/protocol-v4/index.js\");\nconst DCP_ENV = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\n\nconst debugging = __webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope('worker');\nconst { assert } = __webpack_require__(/*! dcp/common/dcp-assert */ \"./src/common/dcp-assert.js\");\nconst { EventEmitter } = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\");\nconst { Sandbox, SandboxError } = __webpack_require__(/*! ./sandbox */ \"./src/dcp-client/worker/sandbox.js\");\nconst { Slice } = __webpack_require__(/*! ./slice */ \"./src/dcp-client/worker/slice.js\");\nconst { SupervisorCache } = __webpack_require__(/*! ./supervisor-cache */ \"./src/dcp-client/worker/supervisor-cache.js\");\nconst { DcpURL } = __webpack_require__(/*! dcp/common/dcp-url */ \"./src/common/dcp-url.js\");\nconst { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\nconst { localStorage } = __webpack_require__(/*! dcp/common/dcp-localstorage */ \"./src/common/dcp-localstorage.js\");\nconst { booley, encodeDataURI, makeSliceURI, leafMerge, asleepMs, shortTime,\n dumpSandboxes, dumpSlices, dumpSandboxesIfNotUnique, dumpSlicesIfNotUnique } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\nconst { calculateJoinHash } = __webpack_require__(/*! dcp/dcp-client/compute-groups */ \"./src/dcp-client/compute-groups/index.js\");\nconst debug = (...args) => {\n if (debugging('supervisor')) {\n console.debug('dcp-client:worker:supervisor', ...args);\n }\n};\n\nconst supervisorTuning = dcpConfig.future('supervisor.tuning');\nconst tuning = {\n watchdogInterval: 7, /**< seconds - time between fetches when ENOTASK(? /wg nov 2019) */\n minSandboxStartDelay: 0.1, /**< seconds - minimum time between WebWorker starts */\n maxSandboxStartDelay: 0.7, /**< seconds - maximum delay time between WebWorker starts */\n ...supervisorTuning\n};\n\n/** Make timers 10x slower when running in niim */\nlet timeDilation = 1;\nif (DCP_ENV.platform === 'nodejs') {\n /** Make timers 10x slower when running in niim */\n timeDilation = (requireNative('module')._cache.niim instanceof requireNative('module').Module) ? 10 : 1;\n}\n\ndcpConfig.future('worker.sandbox', { progressReportInterval: (5 * 60 * 1000) });\nconst sandboxTuning = dcpConfig.worker.sandbox;\n\n/**\n * @typedef {*} opaqueId\n */\n\n/**\n * @typedef {object} SandboxSlice\n * @property {Sandbox} sandbox\n * @property {Slice} slice\n */\n\n/**\n * @typedef {object} Signature\n * @property {Uint8Array} r\n * @property {Uint8Array} s\n * @property {Uint8Array} v\n */\n\n/**\n * @typedef {object} SignedAuthorizationMessageObject\n * @property {object} auth\n * @property {Signature} signature\n * @property {module:dcp/wallet.Address} owner\n */\n\n/** @typedef {import('.').Worker} Worker */\n/** @typedef {import('.').SupervisorOptions} SupervisorOptions */\n\nclass Supervisor extends EventEmitter {\n /**\n * @constructor\n * @param {Worker} worker\n * @param {SupervisorOptions} options\n */\n constructor (worker, options={}) {\n super('Supervisor');\n\n /** @type {Worker} */\n this.worker = worker;\n\n /** @type {Sandbox[]} */\n this.sandboxes = [];\n\n /**\n * XXXpfr @todo We may still needs this.slices to communicate state to result-submitter status endpoint, but it isn't used otherwise.\n * @deprecated\n * @type {Slice[]}\n */\n this.slices = [];\n\n /**\n * The true type is associative array of associative array of SignedAuthorizationMessageObject.\n * How can that be specified in jsdoc?\n * @type {object}\n */\n this.authorizationMessages = {};\n\n /** @type {Slice[]} */\n this.queuedSlices = [];\n\n /** @type {Sandbox[]} */\n this.readiedSandboxes = [];\n\n /** @type {Sandbox[]} */\n this.assignedSandboxes = [];\n\n /** @type {boolean} */\n this.matching = false;\n\n if (!options) {\n console.error('Supervisor Options', options, new Error().stack);\n options = {};\n }\n \n /** @type {object} */\n this.options = {\n jobAddresses: options.jobAddresses || [/* all jobs unless priorityOnly */],\n ...options,\n };\n\n const { paymentAddress, identityKeystore } = options;\n if (paymentAddress) {\n if (paymentAddress instanceof wallet.Keystore) {\n this.paymentAddress = paymentAddress.address;\n } else {\n this.paymentAddress = new wallet.Address(paymentAddress);\n }\n } else {\n this.paymentAddress = null;\n }\n\n this._identityKeystore = identityKeystore;\n\n this.allowedOrigins = dcpConfig.worker.allowOrigins.any;\n if(options.allowedOrigins && options.allowedOrigins.length > 0)\n this.allowedOrigins = options.allowedOrigins.concat(this.allowedOrigins);\n\n /**\n * Maximum sandboxes allowed to work at a given time.\n * @type {number}\n */\n this.maxWorkingSandboxes = options.maxWorkingSandboxes || 1;\n\n /** @type {number} */\n this.defaultMaxGPUs = 1;\n // this.GPUsAssigned = 0;\n \n // Object.defineProperty(this, 'GPUsAssigned', {\n // get: () => this.workingSandboxes.filter(sb => !!sb.requiresGPU).length,\n // enumerable: true,\n // configurable: false,\n // });\n\n /** @type {boolean} */\n this.isFetchingNewWork = false;\n /**\n * TODO: Remove this when the supervisor sends all of the sandbox\n * capabilities to the scheduler when fetching work.\n * @type {object}\n */\n this.capabilities = null;\n\n // This sets an offset into the watchdog bin at which to fire the sweeper\n /** @deprecated - UNUSED */\n this.watchdogSlotTime = options.watchdogInterval || tuning.watchdogInterval * 1000;\n /** @deprecated - UNUSED */\n this.watchdogOffset = Math.random();\n /** @deprecated - UNUSED */\n this.watchdogTimeout = null;\n\n /** @type {number} */\n this.lastProgressReport = 0;\n\n // @hack - dcp-env.isBrowserPlatform is not set unless the platform is _explicitly_ set,\n // using the default detected platform doesn't set it.\n // Fixing that causes an error in the wallet module's startup on web platform, which I\n // probably can't fix in a reasonable time this morning.\n // ~ER2020-02-20\n\n if (!options.maxWorkingSandboxes\n && DCP_ENV.browserPlatformList.includes(DCP_ENV.platform)\n && navigator.hardwareConcurrency > 1) {\n this.maxWorkingSandboxes = navigator.hardwareConcurrency - 1;\n if (typeof navigator.userAgent === 'string') {\n if (/(Android).*(Chrome|Chromium)/.exec(navigator.userAgent)) {\n this.maxWorkingSandboxes = 1;\n console.log('Doing work with Chromimum browsers on Android is currently limited to one sandbox');\n }\n }\n }\n\n /** @type {SupervisorCache} */\n this.cache = new SupervisorCache(this);\n /** @type {object} */\n this._connections = {}; /* active DCPv4 connections */\n // Call the watchdog every 7 seconds.\n this.watchdogInterval = setInterval(() => this.watchdog(), 7000);\n\n const ceci = this;\n\n // Initialize to null so these properties are recognized for the Supervisor class\n this.taskDistributorConnection = null;\n this.eventRouterConnection = null;\n this.resultSubmitterConnection = null;\n this.packageManagerConnection = null;\n this.openTaskDistributorConn = function openTaskDistributorConn()\n {\n let config = dcpConfig.scheduler.services.taskDistributor;\n ceci.taskDistributorConnection = new protocolV4.Connection(config, ceci.identityKeystore, connectionOptions(config.location, 'taskDistributor'));\n ceci.taskDistributorConnection.on('close', ceci.openTaskDistributorConn);\n }\n\n this.openEventRouterConn = function openEventRouterConn()\n {\n let config = dcpConfig.scheduler.services.eventRouter;\n ceci.eventRouterConnection = new protocolV4.Connection(config, ceci.identityKeystore, connectionOptions(config.location, 'eventRouter'));\n ceci.eventRouterConnection.on('close', ceci.openEventRouterConn);\n }\n \n this.openResultSubmitterConn = function openResultSubmitterConn()\n {\n let config = dcpConfig.scheduler.services.resultSubmitter;\n ceci.resultSubmitterConnection = new protocolV4.Connection(config, ceci.identityKeystore, connectionOptions(config.location, 'resultSubmitter'));\n ceci.resultSubmitterConnection.on('close', ceci.openResultSubmitterConn);\n }\n\n this.openPackageManagerConn = function openPackageManagerConn()\n {\n let config = dcpConfig.packageManager;\n ceci.packageManagerConnection = new protocolV4.Connection(config, ceci.identityKeystore, connectionOptions(config.location, 'packageManager'));\n ceci.packageManagerConnection.on('close', ceci.openPackageManagerConn);\n }\n }\n\n /**\n * Return worker opaqueId.\n * @type {opaqueId}\n */\n get workerOpaqueId() {\n if (!this._workerOpaqueId)\n this._workerOpaqueId = localStorage.getItem('workerOpaqueId');\n\n if (!this._workerOpaqueId || this._workerOpaqueId.length !== 22) { /** @todo XXXwg fix magic number problem */\n this._workerOpaqueId = nanoid(22);\n localStorage.setItem('workerOpaqueId', this._workerOpaqueId);\n }\n\n return this._workerOpaqueId;\n }\n\n /**\n * This getter is the absolute source-of-truth for what the\n * identity keystore is for this instance of the Supervisor.\n */\n get identityKeystore() {\n assert(this.defaultIdentityKeystore);\n\n return this._identityKeystore || this.defaultIdentityKeystore;\n }\n\n /**\n * Open all connections. Used when supervisor is instantiated or stopped/started\n * to initially open connections.\n */\n instantiateAllConnections() {\n this.openTaskDistributorConn();\n this.openEventRouterConn();\n this.openResultSubmitterConn();\n this.openPackageManagerConn();\n }\n\n /** Set the default identity keystore -- needs to happen before anything that talks\n * to the scheduler for work gets called. This is a wart and should be removed by\n * refactoring.\n *\n * The default identity keystore will be used if the Supervisor was not provided\n * with an alternate. This keystore will be located via the Wallet API, and \n * if not found, a randomized default identity will be generated. \n *\n * @param {object} ks An instance of wallet::Keystore -- if undefined, we pick the best default we can.\n * @returns {Promise<void>}\n */\n async setDefaultIdentityKeystore(ks) {\n try {\n if (ks) {\n this.defaultIdentityKeystore = ks;\n return;\n }\n\n if (this.defaultIdentityKeystore)\n return;\n\n try {\n this.defaultIdentityKeystore = await wallet.getId();\n } catch(e) {\n debugging('supervisor') && console.debug('supervisor: generating default identity', this.defaultIdentityKeystore.address);\n this.defaultIdentityKeystore = await new wallet.IdKeystore(null, '');\n }\n } finally {\n debugging('supervisor') && console.debug('supervisor: set default identity =', this.defaultIdentityKeystore.address);\n }\n }\n \n //\n // What follows is a bunch of utility functions for creating filtered views\n // of the slices and sandboxes array.\n //\n\n /**\n * Sandboxes that are in WORKING state.\n * Warning: Do not rely on this information being 100% accurate -- it may change in the next instant.\n * @type {Sandbox[]}\n */\n get workingSandboxes() {\n return this.sandboxes.filter(sandbox => sandbox.isWorking);\n }\n\n /**\n * This property is used as the target number of sandboxes to be associated with slices and transition sandbox-state from ASSIGNED -> WORKING.\n *\n * It is used in this.watchdog as to prevent a call to this.work when unallocatedSpace <= 0.\n * It is also used in this.distributeQueuedSlices where it is passed as an argument to this.matchSlicesWithSandboxes to indicate how many sandboxes\n * to associate with slices and transition sandbox-state from ASSIGNED -> WORKING.\n *\n * Warning: Do not rely on this information being 100% accurate -- it may change in the next instant.\n * @type {number}\n */\n get unallocatedSpace() {\n return this.maxWorkingSandboxes - this.sandboxes.filter(sandbox => sandbox.allocated).length;\n }\n\n /**\n * Slices that have SLICE_STATUS_UNASSIGNED status.\n * Warning: Do not rely on this information being 100% accurate -- it may change in the next instant.\n * @type {Slice[]}\n */\n get snapshotOfQueuedSlices() {\n return this.slices.filter(slice => slice.isUnassigned);\n }\n\n /**\n * Slices that have SLICE_STATUS_WORKING status.\n * Warning: Do not rely on this information being 100% accurate -- it may change in the next instant.\n * @type {Slice[]}\n */\n get snapshotOfWorkingSlices() {\n return this.slices.filter(slice => slice.isWorking)\n }\n\n /**\n * Sandboxes that are in READY_FOR_ASSIGN state.\n * Warning: Do not rely on this information being 100% accurate -- it may change in the next instant.\n * @type {Sandbox[]}\n */\n get snapshotOfReadiedSandboxes() {\n return this.sandboxes.filter(sandbox => sandbox.isReadyForAssign);\n }\n\n /**\n * Sandboxes that are in ASSIGNED state.\n * Warning: Do not rely on this information being 100% accurate -- it may change in the next instant.\n * @type {Sandbox[]}\n */\n get snapshotOfAssignedSandboxes() {\n return this.sandboxes.filter(sandbox => sandbox.isAssigned);\n }\n\n /**\n * Sandboxes that are in TERMINATED state.\n * Warning: Do not rely on this information being 100% accurate -- it may change in the next instant.\n * @type {Sandbox[]}\n */\n get snapshotOfTerminatedSandboxes() {\n return this.sandboxes.filter(sandbox => sandbox.isTerminated);\n }\n\n /**\n * Remove element from theArray.\n * @parameter {Array<*>} theArray\n * @parameter {object|number} element\n * @parameter {boolean} [assertExists = true]\n */\n removeElement(theArray, element, assertExists = false) {\n let index = theArray.indexOf(element);\n assert(index !== -1 || !assertExists);\n if (index !== -1) theArray.splice(index, 1);\n }\n\n /**\n * Check authorization message wrt jobAddress and sliceNumber.\n * @parameter {string} text\n * @parameter {opaqueId} jobAddress\n * @parameter {number} sliceNumber\n * @returns {SignedAuthorizationMessageObject}\n */\n checkAuthorization(text, jobAddress, sliceNumber) {\n const authorizationMessage = this.authorizationMessages[jobAddress][sliceNumber];\n if (!authorizationMessage)\n console.log(`\\t${text} DANGER: Undefined authorization. ${jobAddress}, ${sliceNumber}`);\n return authorizationMessage;\n }\n\n /**\n * If the elements of sandboxSliceArray are not unique, log the duplicates and dump the array.\n * @parameter {SandboxSlice[]} sandboxSliceArray\n * @parameter {string} header\n */\n dumpSandboxSlicesIfNotUnique(sandboxSliceArray, header) {\n if (!this.isUniqueSandboxSlices(sandboxSliceArray, header))\n this.dumpSandboxSlices(sandboxSliceArray);\n }\n\n /**\n * Log sandboxSlice.\n * @parameter {SandboxSlice} sandboxSlice\n * @returns {string}\n */\n dumpSandboxSlice(sandboxSlice) {\n return `${sandboxSlice.sandbox.id}.${sandboxSlice.slice.sliceNumber}.${sandboxSlice.slice.jobAddress}`;\n }\n\n /**\n * Log { sandbox, slice }.\n * @parameter {Sandbox} sandbox\n * @parameter {Slice} slice\n * @returns {string}\n */\n dumpSandboxAndSlice(sandbox, slice) {\n return this.dumpSandboxSlice({ sandbox, slice });\n }\n\n /**\n * Dump sandboxSliceArray.\n * @parameter {SandboxSlice[]} sandboxSliceArray\n * @parameter {string} header\n */\n dumpSandboxSlices(sandboxSliceArray, header) {\n if (header) console.log(`\\n${header}`);\n sandboxSliceArray.forEach(x => console.log(`\\t{ ${x.sandbox.id}.${x.sandbox.jobAddress}.${x.sandbox.state}, ${x.slice.sliceNumber}.${x.slice.jobAddress}.${x.slice.status} }`));\n }\n\n /**\n * Check sandboxSliceArray for duplicates.\n * @parameter {SandboxSlice[]} sandboxSliceArray\n * @parameter {string} header\n * @parameter {function} log\n * @returns {boolean}\n */\n isUniqueSandboxSlices(sandboxSliceArray, header, log) {\n return sandboxSliceArray.length === this.makeUniqueSandboxSlices(sandboxSliceArray, header, log).length;\n }\n\n /**\n * Returns a copy of sandboxSliceArray with all duplicates removed.\n * @parameter {SandboxSlice[]} sandboxSliceArray\n * @parameter {string} header\n * @parameter {function} log\n * @returns {SandboxSlice[]}\n */\n makeUniqueSandboxSlices(sandboxSliceArray, header, log) {\n const result = [], slices = [], sandboxes = [];\n let once = true;\n sandboxSliceArray.forEach(x => {\n const sliceIndex = slices.indexOf(x.slice);\n const sandboxIndex = sandboxes.indexOf(x.sandbox);\n\n if (sandboxIndex >= 0) {\n if (once && header) console.log(`\\n${header}`); once = false;\n log ? log(x.sandbox) : console.log(`\\tDANGER: Found duplicate sandbox ${x.sandbox.id}.${x.sandbox.jobAddress}.${x.sandbox.state}.`);\n } else sandboxes.push(x.sandbox);\n\n if (sliceIndex >= 0) {\n if (once && header) console.log(`\\n${header}`); once = false;\n log ? log(x.slice) : console.log(`\\tDANGER: Found duplicate slice ${x.slice.sliceNumber}.${x.slice.jobAddress}.${x.slice.status}.`);\n } else {\n slices.push(x.slice);\n if (sandboxIndex < 0) result.push(x);\n }\n });\n return result;\n }\n\n /** NOT used ATM, it's faster to just filter the assigned sandboxes for a jobAddress on-demand\n * \n * Bins the assigned sandboxes in an object, keyed by their jobAddress.\n * { 0xgenAddress1: sandboxes[], 0xgenAddress2: sandboxes[], ... }\n * @type {object}\n */\n get assignedSandboxesSorted () {\n return this.assignedSandboxes.reduce((o, w) => {\n if (!w.jobAddress) throw new Error(\"Assigned sandbox doesn't have a job opaque id\", w);\n\n if (w.jobAddress in o) {\n o[w.jobAddress].push(w);\n } else o[w.jobAddress] = [w];\n\n return o;\n }, {});\n }\n\n /**\n * Attempts to create and start a given number of sandboxes.\n * The sandboxes that are created can then be assigned for a\n * specific job at a later time. All created sandboxes\n * get put into the @this.readiedSandboxes array.\n *\n * @param {number} numSandboxes - the number of sandboxes to create\n * @returns {Promise<Sandbox[]>} - resolves with array of created sandboxes, rejects otherwise\n * @throws when given a numSandboxes is not a number > 0 or if numSandboxes is Infinity\n */\n async readySandboxes (numSandboxes) {\n debug('Readying sandboxes');\n if (typeof numSandboxes !== 'number' || Number.isNaN(numSandboxes) || numSandboxes === Infinity) {\n throw new Error(`${numSandboxes} is not a number of sandboxes that can be readied.`);\n }\n if (numSandboxes <= 0) {\n return [];\n }\n\n const sandboxStartPromises = [];\n const sandboxes = [];\n const errors = [];\n for (let i = 0; i < numSandboxes; i++) {\n const sandbox = new Sandbox(this.cache, {\n ...this.options.sandboxOptions,\n }, this.allowedOrigins);\n sandbox.addListener('ready', () => this.emit('sandboxReady', sandbox));\n sandbox.addListener('start', () => {\n this.emit('sandboxStart', sandbox);\n\n // When sliceNumber == 0, result-submitter status skips the slice,\n // so don't send it in the first place.\n // The 'start' event is fired when a worker starts up, hence there's no way\n // to determine whether sandbox has a valid slice without checking.\n if (sandbox.slice) {\n const jobAddress = sandbox.jobAddress;\n const sliceNumber = sandbox.slice.sliceNumber;\n const authorizationMessage = this.checkAuthorization('Supervisor.readySandboxes sandboxStart:', jobAddress, sliceNumber);\n if (!authorizationMessage) return;\n\n this.resultSubmitterConnection.send('status', {\n worker: this.workerOpaqueId,\n slices: [{\n job: jobAddress,\n sliceNumber: sliceNumber,\n status: 'begin',\n authorizationMessage,\n }],\n });\n }\n });\n sandbox.addListener('workEmit', ({ eventName, payload }) => {\n // Need to check if the sandbox hasn't been assigned a slice yet.\n if (!sandbox.slice) {\n if (__webpack_require__(/*! dcp/common/dcp-build */ \"./src/common/dcp-build.js\").build === \"debug\") {\n console.error(\n `Sandbox not assigned a slice before sending workEmit message to scheduler. 'workEmit' event originates from \"${eventName}\" event`, \n payload,\n );\n }\n }\n\n const jobAddress = sandbox.slice.jobAddress;\n const sliceNumber = sandbox.slice.sliceNumber;\n // sliceNumber can be zero if it came from a problem with loading modules.\n assert(jobAddress && (sliceNumber || sliceNumber === 0));\n try {\n /* Make sure we actually have an authorization message at jobAddress and sliceNumber. */\n const authorizationMessage = this.checkAuthorization('Supervisor.readySandboxes workEmit:', jobAddress, sliceNumber);\n if (!authorizationMessage) return;\n\n /* DCP-1698 Send auth msg with tasks to worker, then validate authority of worker to send slice info back to scheduler. */\n this.eventRouterConnection.send('workEmit', {\n eventName,\n payload,\n job: jobAddress,\n slice: sliceNumber,\n worker: this.workerOpaqueId,\n authorizationMessage,\n });\n } catch (error) {\n debugging('supervisor') &&\n console.debug(\n `Authorizing failed: jobAddress: ${jobAddress}, slice: ${sliceNumber}, error: ${error}`,\n );\n }\n });\n\n // When any sbx completes, \n sandbox.addListener('complete', () => {\n // this.distributeQueuedSlices();\n this.watchdog();\n //this.work();\n });\n\n const delayMs =\n 1000 *\n (tuning.minSandboxStartDelay +\n Math.random() *\n (tuning.maxSandboxStartDelay - tuning.minSandboxStartDelay));\n \n sandboxStartPromises.push(\n sandbox\n .start(delayMs)\n .then(() => {\n this.sandboxes.push(sandbox);\n })\n .catch((err) => {\n errors.push(err);\n if(err.code === 'ENOWORKER'){\n throw new DCPError(\"Cannot use localExec without dcp-worker installed. Use the command 'npm install dcp-worker' to install the neccessary modules.\", 'ENOWORKER');\n }\n }));\n sandboxes.push(sandbox);\n }\n\n await Promise.all(sandboxStartPromises);\n\n if (errors.length) {\n console.warn(`Failed to ready ${errors.length} of ${numSandboxes} sandboxes.`, errors);\n throw new Error('Failed to ready sandboxes.');\n }\n\n if (this.readiedSandboxes.length > 0)\n sandboxes.push(...this.readiedSandboxes);\n this.readiedSandboxes = sandboxes;\n\n debugging() && console.log(`readySandboxes: all readied sandboxes ${JSON.stringify(this.readiedSandboxes.map(sandbox => sandbox.id))}`);\n \n return sandboxes;\n }\n\n /**\n * Accepts a sandbox after it has finished working or encounters an error.\n * If the sandbox was terminated or if \"!slice || slice.failed\" then\n * the sandbox will be removed from the sandboxes array and terminated if necessary.\n * Otherwise it will try to distribute a slice to the sandbox immediately.\n *\n * @param {Sandbox} sandbox - the sandbox to return\n * @param {Slice} [slice] - the slice just worked on; !slice => terminate\n * @returns {Promise<void>}\n */\n async returnSandbox (sandbox, slice) {\n if (!slice || slice.failed || sandbox.isTerminated) {\n this.removeElement(this.sandboxes, sandbox);\n if (!sandbox.isTerminated) {\n sandbox.terminate(false);\n }\n }\n\n if (slice) {\n\n const sliceNumber = slice.sliceNumber;\n const jobAddress = slice.jobAddress;\n\n //\n // An upstream failure like certain connection failures, can cause an error cascade,\n // so we need to gaurd against indeterminant state with this conditional.\n // An example of such an error cascade is calling Connection.fetchTransport\n // when this.connectionOptions.transports is empty.\n //\n if (this.authorizationMessages[jobAddress][sliceNumber]) {\n debugging() && console.log(`Supervisor.returnSandbox: ${jobAddress}, ${sliceNumber}, kill corresponding auth message.`);\n /* Done with slice. Cleanup. */\n delete this.authorizationMessages[jobAddress][sliceNumber];\n if (Object.keys(this.authorizationMessages[jobAddress]).length === 0) {\n delete this.authorizationMessages[jobAddress];\n }\n }\n }\n }\n\n /**\n * Terminates sandboxes, in order of creation, when the total started sandboxes exceeds the total allowed sandboxes.\n *\n * @todo Check through the currently started sandboxes and attempt to terminate\n * ones that aren't frequently used.\n * ^^^ This todo may be partially done due to terminating older sandboxes - RR Nov 2019\n * @returns {Promise<void>}\n */\n async pruneSandboxes () {\n let numOver = this.sandboxes.length - (dcpConfig.worker.maxAllowedSandboxes + this.maxWorkingSandboxes);\n if (numOver <= 0 || this.matching) return; // Don't kill sandboxes while matching with slices.\n \n let index = 0;\n while (index < this.readiedSandboxes.length) {\n const sandbox = this.readiedSandboxes[index];\n // if this sbx is allocated, skip it and look at the next one\n if (sandbox.allocated) {\n index++;\n continue;\n }\n // otherwise, remove this one and look at the same array element next loop\n debugging() && console.log(`pruneSandboxes: Killing readied sandbox ${sandbox.id}`);\n this.readiedSandboxes.splice(index, 1);\n await this.returnSandbox(sandbox);\n\n if (--numOver <= 0) break;\n }\n\n\n if (numOver <= 0) return;\n index = 0;\n while (index < this.assignedSandboxes.length) {\n const sandbox = this.assignedSandboxes[index];\n \n // if the sandbox is allocated, advance to the next one in the list\n if (sandbox.allocated) {\n index++;\n continue;\n }\n \n // otherwise, remove this sandbox but look at the same array index in the next loop\n debugging() && console.log(`pruneSandboxes: Killing assigned sandbox ${sandbox.id}`);\n this.assignedSandboxes.splice(index, 1);\n await this.returnSandbox(sandbox);\n\n if (--numOver <= 0) break;\n }\n }\n \n /**\n * Basic watch dog to check if there are idle sandboxes and\n * attempts to nudge the supervisor to feed them work.\n *\n * Run in an interval created in @constructor .\n * @returns {Promise<void>}\n */\n async watchdog () {\n if (!this.watchdogState)\n this.watchdogState = {};\n \n // Every 5 minutes, report progress of all working slices to the scheduler\n if (Date.now() > ((this.lastProgressReport || 0) + sandboxTuning.progressReportInterval)) {\n // console.log('454: Assembling progress update...');\n this.lastProgressReport = Date.now();\n \n const slices = [];\n \n this.queuedSlices.forEach(slice => {\n const jobAddress = slice.jobAddress;\n const sliceNumber = slice.sliceNumber;\n const authorizationMessage = this.checkAuthorization('Supervisor.watchdog:', jobAddress, sliceNumber);\n\n if (authorizationMessage) {\n addToSlicePayload(slices, jobAddress, sliceNumber, 'scheduled', authorizationMessage);\n }\n })\n \n this.workingSandboxes.forEach(sb => {\n // The lifetime of sandbox.isWorking is (or should be) precisely defined,\n // so we can assume sb.slice is always valid here.\n assert(sb.slice);\n\n const jobAddress = sb.jobAddress;\n const sliceNumber = sb.slice.sliceNumber;\n const authorizationMessage = this.checkAuthorization('Supervisor.watchdog:', jobAddress, sliceNumber);\n\n if (authorizationMessage) {\n addToSlicePayload(slices, jobAddress, sliceNumber, 'progress', authorizationMessage);\n }\n });\n\n if (slices.length) {\n // console.log('471: sending progress update...');\n const progressReportPayload = {\n worker: this.workerOpaqueId,\n slices,\n };\n\n this.resultSubmitterConnection.send('status', progressReportPayload)\n .catch(error => {\n console.error('479: Failed to send status update:', error/*.message*/);\n });\n }\n }\n\n if (this.worker.working) {\n if (this.unallocatedSpace > 0) {\n await this.work().catch(err => {\n if (!this.watchdogState[err.code || '0'])\n this.watchdogState[err.code || '0'] = 0;\n if (Date.now() - this.watchdogState[err.code || '0'] > ((dcpConfig.worker.watchdogLogInterval * timeDilation || 120) * 1000))\n console.error('301: Failed to start work:', err);\n this.watchdogState[err.code || '0'] = Date.now();\n });\n }\n\n this.pruneSandboxes();\n }\n }\n\n /**\n * Gets the logical and physical number of cores and also\n * the total number of sandboxes the worker is allowed to run\n *\n */\n getStatisticsCPU() {\n if (DCP_ENV.isBrowserPlatform) {\n return {\n worker: this.workerOpaqueId,\n lCores: window.navigator.hardwareConcurrency,\n pCores: dcpConfig.worker.pCores || window.navigator.hardwareConcurrency,\n sandbox: this.maxWorkingSandboxes\n }\n }\n\n return {\n worker: this.workerOpaqueId,\n lCores: requireNative('os').cpus().length,\n pCores: requireNative('physical-cpu-count'),\n sandbox: this.maxWorkingSandboxes\n }\n }\n\n /**\n * Call to start doing work on the network.\n * This is the one place where requests to fetch new slices are made.\n * After the initial slice and job are fetched it calls @this.distributeQueuedSlices.\n *\n * @returns {Promise<void>}\n */\n async work()\n {\n\n await this.setDefaultIdentityKeystore();\n\n // if connections don't exist, instantiate them. Needed every time worker is started/stopped\n if (!this.taskDistributorConnection)\n this.instantiateAllConnections();\n\n let slicesToFetch;\n if (this.options.priorityOnly && this.options.jobAddresses.length === 0) {\n slicesToFetch = 0;\n } else if (this.queuedSlices.length > 1) {\n // We have slices queued, no need to fetch\n slicesToFetch = 0;\n } else {\n // The queue is almost empty (there may be 0 or 1 element), fetch a full task.\n // The task is full, in the sense that it will contain slices whose\n // aggregate execution time is this.maxWorkingSandboxes * 5-minutes.\n // However, there can only be this.unallocatedSpace # of long slices.\n // Thus we need to know whether a slice in this.queuedSlices is long or not.\n // (A long slice has estimated execution time > 5-minutes.)\n const longSliceCount = (this.queuedSlices.length > 0 && this.queuedSlices[0].isLongSlice) ? 1 : 0;\n slicesToFetch = this.unallocatedSpace - longSliceCount;\n }\n\n debugging() && console.log(`Supervisor.work: Try to get ${slicesToFetch} slices in working sandboxes, unallocatedSpace ${this.unallocatedSpace}, queued slices ${this.queuedSlices.length}`);\n \n // Fetch a new task if we have no more slices queued, then start workers\n try {\n if (slicesToFetch > 0 && !this.isFetchingNewWork) {\n this.isFetchingNewWork = true;\n\n /**\n * This will only ready sandboxes up to a total count of\n * maxWorkingSandboxes (in any state). It is not possible to know the\n * actual number of sandboxes required until we have the slices because we\n * may have sandboxes assigned for the slice's job already.\n */\n if (this.maxWorkingSandboxes > this.sandboxes.length) {\n await this.readySandboxes(\n this.maxWorkingSandboxes - this.sandboxes.length,\n );\n }\n\n /**\n * Temporary change: Assign the capabilities of one of readied sandboxes\n * before fetching slices from the scheduler.\n *\n * TODO: Remove this once fetchTask uses the capabilities of every\n * sandbox to fetch slices.\n */\n if (!this.capabilities) {\n this.capabilities = this.sandboxes[0].capabilities;\n this.emit('capabilitiesCalculated', this.capabilities);\n }\n\n if(DCP_ENV.isBrowserPlatform && this.capabilities.browser)\n this.capabilities.browser.chrome = DCP_ENV.isBrowserChrome;\n\n const fetchTimeout = setTimeout(() => {\n console.warn(`679: Fetch exceeded timeout, will retry at next watchdog interval`);\n this.isFetchingNewWork = false;\n }, 3 * 60 * 1000); // max out at 3 minutes to fetch\n\n // ensure result submitter connection before fetching tasks\n try\n {\n await this.resultSubmitterConnection.keepalive();\n }\n catch (error)\n {\n console.error('Failed to connect to result submitter, refusing to fetch slices. Will try again at next fetch cycle.')\n debugging('supervisor') && console.log(`Error: ${error}`);\n this.isFetchingNewWork = false;\n return;\n }\n await this.fetchTask(slicesToFetch);\n clearTimeout(fetchTimeout);\n }\n \n this.distributeQueuedSlices().then(() => debugging('supervisor') && 'supervisor: finished distributeQueuedSlices()').catch((e)=>console.error(e)) ;\n// await this.distributeQueuedSlices();\n // No catch(), because it will bubble outward to the caller\n } finally {\n this.isFetchingNewWork = false;\n }\n }\n\n /**\n * Generate the workerComputeGroups property of the requestTask message. \n * \n * Concatenate the compute groups object from dcpConfig with the list of compute groups\n * from the supervisor, and remove the public group if accidentally present. Finally,\n * we transform joinSecrets/joinHash into joinHashHash for secure transmission.\n *\n * @note computeGroup objects with joinSecrets are mutated to record their hashes. This\n * affects the supervisor options and dcpConfig. Re-adding a joinSecret property\n * to one of these will cause the hash to be recomputed.\n */\n generateWorkerComputeGroups()\n {\n var computeGroups = Object.values(dcpConfig.worker.computeGroups || {});\n if (this.options.computeGroups)\n computeGroups = computeGroups.concat(this.options.computeGroups);\n computeGroups = computeGroups.filter(group => group.id !== constants.computeGroups.public.id);\n const hashedComputeGroups = [];\n for (const group of computeGroups)\n {\n const groupCopy = Object.assign({}, group);\n if ((group.joinSecret || group.joinHash) && (!group.joinHashHash || this.lastDcpsid !== this.taskDistributorConnection.dcpsid))\n {\n let joinHash;\n if(group.joinHash) {\n joinHash = group.joinHash;\n } else {\n joinHash = calculateJoinHash(groupCopy);\n } \n\n groupCopy.joinHashHash = hash.calculate(hash.eh1, joinHash, this.taskDistributorConnection.dcpsid);\n delete groupCopy.joinSecret;\n delete groupCopy.joinHash;\n debugging('computeGroups') && console.debug(`Calculated joinHash=${joinHash} for`, groupCopy);\n }\n hashedComputeGroups.push(groupCopy);\n }\n this.lastDcpsid = this.taskDistributorConnection.dcpsid;\n debugging('computeGroups') && console.debug('Requesting ', computeGroups.length, 'non-public groups for session', this.lastDcpsid);\n return hashedComputeGroups;\n }\n\n /**\n * Fetches a task, which contains job information and slices for sandboxes and\n * manages events related to fetching tasks so the UI can more clearly display\n * to user what is actually happening.\n * @param {number} numCores\n * @returns {Promise<void>} The requestTask request, resolve on success, rejects otherwise.\n * @emits Supervisor#fetchingTask\n * @emits Supervisor#fetchedTask\n */\n async fetchTask(numCores)\n {\n this.emit('fetchingTask');\n debugging('supervisor') && console.debug('supervisor: fetching task');\n const requestPayload = {\n numCores,\n coreStats: this.getStatisticsCPU(),\n numGPUs: this.defaultMaxGPUs,\n capabilities: this.capabilities,\n paymentAddress: this.paymentAddress,\n jobAddresses: this.options.jobAddresses, // when set, only fetches slices for these jobs\n localExec: this.options.localExec,\n workerComputeGroups: this.generateWorkerComputeGroups(),\n minimumWage: dcpConfig.worker.minimumWage || this.options.minimumWage,\n readyJobs: [ /* list of jobs addresses XXXwg */ ],\n previouslyWorkedJobs: this.cache.jobs,\n };\n\n // workers should be part of the public compute group by default\n if (!booley(dcpConfig.worker.leavePublicGroup) && !booley(this.options.leavePublicGroup))\n requestPayload.workerComputeGroups.push(constants.computeGroups.public);\n\n debugging('computeGroups') && console.log(`Fetching work for ${requestPayload.workerComputeGroups.length} ComputeGroups: `, requestPayload.workerComputeGroups);\n \n debugging() && console.log(`${shortTime()} fetchTask wants ${numCores} slices, unallocatedSpace ${this.unallocatedSpace}, queuedSlices ${this.queuedSlices.length}`);\n try {\n debugging('requestTask') && debug('requestPayload', requestPayload);\n\n let result = await this.taskDistributorConnection.send('requestTask', requestPayload);\n let responsePayload = result.payload; \n \n if (!result.success) { \n debugging() && console.log('Task fetch failure; request=', requestPayload);\n debugging() && console.log('Task fetch failure; response=', result.payload);\n throw new DCPError('Unable to fetch task for worker', responsePayload);\n }\n\n const sliceCount = responsePayload.body.task.length || 0;\n\n /**\n * The fetchedTask event fires when the supervisor has finished trying to\n * fetch work from the scheduler (task-manager). The data emitted is the\n * number of new slices to work on in the fetched task.\n *\n * @event Supervisor#fetchedTask\n * @type {number}\n */\n this.emit('fetchedTask', sliceCount);\n\n if (sliceCount < 1) {\n return;\n }\n\n /*\n * DCP-1698 Send auth msg with tasks to worker, then validate authority of worker to send slice info back to scheduler.\n * payload structure: { owner: this.address, signature: signature, auth: messageLightWeight, body: messageBody };\n * XXXpfr: @todo Do not think we need 'owner', think about it.\n */\n const { body, ...authorizationMessage } = responsePayload;\n const { newJobs, task } = body;\n assert(newJobs); // It should not be possible to have !newJobs -- we throw on !success.\n for (const jobAddress of Object.keys(newJobs)) {\n this.cache.store('job', jobAddress, newJobs[jobAddress]);\n }\n debugging('supervisor') && console.log(`${shortTime()} fetchTask: Task has ${task.length} slices from ${Object.keys(newJobs).length} new jobs.`);\n\n const tmpQueuedSlices = [];\n for (const taskElement of task) {\n\n if (!this.authorizationMessages[taskElement.jobAddress]) {\n this.authorizationMessages[taskElement.jobAddress] = {};\n // All workers are authorized to send messages to the '0' slice if it is assigned the job.\n this.authorizationMessages[taskElement.jobAddress][0] = authorizationMessage;\n }\n this.authorizationMessages[taskElement.jobAddress][taskElement.sliceNumber] = authorizationMessage;\n\n tmpQueuedSlices.push(new Slice(taskElement));\n debugging('supervisor') && console.log(`/t${shortTime()} fetchTask: slice ${taskElement.sliceNumber}, jobAddress ${taskElement.jobAddress}`);\n }\n\n /** XXXpfr @todo Try to get rid of this.slices. */\n this.slices.push(...tmpQueuedSlices);\n\n this.queuedSlices = [...tmpQueuedSlices, ...this.queuedSlices]; /* Make sure the old ones are up front. */\n\n } catch (error) {\n this.emit('fetchTaskFailed', error);\n debugging('supervisor') && console.debug(`Supervisor.fetchTask failed!: error: ${error}`);\n }\n }\n\n /**\n * For each slice in this.queuedSlices, match with a sandbox in the following order:\n * 1. Try to find an already assigned sandbox in this.assignedSandboxes for the slice's job.\n * 2. Find a ready sandbox in this.readiedSandboxes that is unassigned.\n * 3. Ready a new sandbox and use that.\n *\n * Take great care in assuring sandboxes and slices are uniquely associated, viz.,\n * a given slice cannot be associated with multiple sandboxes and a given sandbox cannot be associated with multiple slices.\n * The lack of such uniqueness has been the root cause of several difficult bugs.\n *\n * @param {number} numSlices The number of slices to match with sandboxes.\n * @returns {Promise<SandboxSlice[]>} Returns SandboxSlice[], may have length zero.\n */\n async matchSlicesWithSandboxes (numSlices) {\n const sandboxSlices = [];\n if (numSlices <= 0 || this.queuedSlices.length == 0 || this.matching)\n return sandboxSlices;\n\n try\n {\n this.matching = true;\n const jobSliceMap = {};\n const jobs = [];\n\n // Until this is rock-solid-stable I want to check for uniquenees.\n if (true) {\n dumpSlicesIfNotUnique(this.queuedSlices, `DANGER: this.queuedSlices slices are not unique -- this is ok when slice is rescheduled.`);\n dumpSandboxesIfNotUnique(this.readiedSandboxes, `DANGER: this.readiedSandboxes sandboxes are not unique!`);\n dumpSandboxesIfNotUnique(this.assignedSandboxes, `DANGER: this.assignedSandboxes sandboxes are not unique!`);\n }\n\n // 'false' is so it is easy to turn back to 'true'\n // Since we don't know yet whether these changes are rock-solid-stable I want to keep easy-to-debug stuff like this.\n if ( false || debugging()) {\n console.log(`${shortTime()} matchSlicesWithSandboxes: numSlices ${numSlices}, numQueuedSlices ${this.queuedSlices.length}: numAssignedSandboxes ${this.assignedSandboxes.length}, numReadySandboxes ${this.readiedSandboxes.length}, total sandbox count: ${this.sandboxes.length}`);\n dumpSlices(this.queuedSlices, 'matchSlicesWithSandboxes(top): this.queuedSlices');\n dumpSandboxes(this.assignedSandboxes, 'matchSlicesWithSandboxes(top): this.assignedSandboxes');\n dumpSandboxes(this.readiedSandboxes, 'matchSlicesWithSandboxes(top): this.readiedSandboxes');\n }\n\n // Partition slices to be parameterized by slice.jobAddress.\n let counter = 0, readyCounter = 0, newCounter = 0;\n while (this.queuedSlices.length > 0) {\n const slice = this.queuedSlices.pop();\n if (!slice.isUnassigned) continue;\n if (!jobSliceMap[slice.jobAddress]) jobSliceMap[slice.jobAddress] = [];\n jobSliceMap[slice.jobAddress].push(slice);\n if (++counter >= numSlices) break;\n }\n counter = 0;\n\n for (const [jobAddress, slicesForJob] of Object.entries(jobSliceMap)) {\n jobs.push(jobAddress);\n\n // First get all assigned sandboxes.\n const assignedSandboxesForJob = this.assignedSandboxes.filter(sandbox => sandbox.jobAddress === jobAddress);\n\n while (assignedSandboxesForJob.length > 0 && slicesForJob.length > 0) {\n counter++;\n const sandbox = assignedSandboxesForJob.pop();\n this.removeElement(this.assignedSandboxes, sandbox);\n const slice = slicesForJob.pop();\n assert(slice.jobAddress === sandbox.jobAddress);\n debugging() && console.log(`matchSlicesWithSandboxes: assigned sb matched ${this.dumpSandboxAndSlice(sandbox, slice)}`);\n sandboxSlices.push({ sandbox, slice });\n }\n\n // Next get existing and possibly new ready-for-assign sandboxes.\n while (slicesForJob.length > 0) {\n while (this.readiedSandboxes.length > 0 && slicesForJob.length > 0) {\n readyCounter++;\n const slice = slicesForJob.pop();\n const sandbox = this.readiedSandboxes.pop();\n debugging() && console.log(`matchSlicesWithSandboxes: readied sb matched ${this.dumpSandboxAndSlice(sandbox, slice)}`);\n sandboxSlices.push({ sandbox, slice });\n }\n\n if (slicesForJob.length > 0) {\n newCounter++;\n await this.readySandboxes(slicesForJob.length);\n } else {\n readyCounter -= newCounter;\n break;\n }\n }\n }\n\n debugging() && console.log(`matchSlicesWithSandboxes: matches: ${ JSON.stringify(sandboxSlices.map(ss => this.dumpSandboxSlice(ss))) }`);\n\n // Until this is rock-solid-stable I want to check for uniquenees.\n if (true) {\n this.dumpSandboxSlicesIfNotUnique(sandboxSlices, `DANGER: sandboxSlices; { sandbox, slice } pairs are not unique!`);\n }\n\n // 'false' is so it is easy to turn back to 'true'\n // Since we don't know yet whether these changes are rock-solid-stable I want to keep easy-to-debug stuff like this.\n if ( false || debugging()) {\n console.log(`matchSlicesWithSandboxes: found ${sandboxSlices.length} sandboxes for jobs ${JSON.stringify(jobs)}: assigned ${counter}, ready ${readyCounter}, new ${newCounter}`);\n this.dumpSandboxSlices(sandboxSlices, 'matchSlicesWithSandboxes: sandboxSlices');\n dumpSandboxes(this.assignedSandboxes, 'matchSlicesWithSandboxes: this.assignedSandboxes');\n dumpSandboxes(this.readiedSandboxes, 'matchSlicesWithSandboxes: this.readiedSandboxes');\n console.log(`matchSlicesWithSandboxes: remaining readied: ${JSON.stringify(this.readiedSandboxes.map(s => s.id))}`);\n }\n\n sandboxSlices.forEach(sandboxSlice => {\n assert(!sandboxSlice.sandbox.allocated);\n sandboxSlice.sandbox.allocated = true;\n });\n\n } catch (e) {\n console.error(`DANGER matchSlicesWithSandboxes threw exception`, e);\n //throw e; // Should we rethrow?\n } finally {\n this.matching = false;\n }\n\n debugging() && console.log(`${shortTime()} matchSlicesWithSandboxes allocated ${sandboxSlices.length} sandboxes, queuedSlices ${this.queuedSlices.length}, unallocatedSpace ${this.unallocatedSpace}.`);\n\n return sandboxSlices;\n }\n\n /**\n * Searches for a sandbox that is eligible to work on the given slice.\n * It will search for a sandbox in the following order:\n * 1. Try to find an already assigned sandbox for the slice's job\n * 2. Find a ready sandbox that is unassigned\n * 3. Ready a new sandbox and use that\n * @deprecated This is a high-powered foot-gun.\n * @param {Slice} slice The slice for which to find a sandbox.\n * @returns {Promise<Sandbox|null>} Returns the sandbox instance if matched, otherwise returns null.\n */\n async findSandboxForSlice (slice) {\n let sandbox;\n let assignedSandboxesForJob = this.snapshotOfAssignedSandboxes.filter(w => w.jobAddress === slice.jobAddress);\n\n switch (true) {\n case !!(sandbox = assignedSandboxesForJob.pop()):\n case !!(sandbox = this.snapshotOfReadiedSandboxes.pop()):\n case !!(sandbox = (await this.readySandboxes(1)).pop()):\n return sandbox;\n default:\n return null;\n }\n }\n\n /**\n * Given an assigned sandbox, it tries to find a slice that the sandbox can work on.\n * Unused.\n * @deprecated Foot-gun.\n * @param {Sandbox} sandbox The assigned sandbox\n * @returns {Slice|undefined} Returns a slice in case of a match, undefined otherwise\n */\n findSliceForSandbox (sandbox) {\n return this.queuedSlices.find((slice) => slice.jobAddress === sandbox.jobAddress);\n }\n\n /**\n * This method will call @this.startSandboxWork(sandbox, slice) for { sandbox, slice }\n * in the array returned by @this.matchSlicesWithSandboxes(availableSandboxes) until all sandboxes are working.\n * It is possible for a sandbox to finish simultaneously and leave a sandbox that is not working.\n * Note: @this.queuedSlices may be exhausted before all sandboxes are working.\n * @returns {Promise<void>}\n */\n async distributeQueuedSlices () {\n //\n // All matching of sandbox with slice is taken care of by @this.matchSlicesWithSandboxes().\n // We do not use @this.snapshotOfQueuedSlices becuase it is in a fuzzy state; when matched with a sandbox,\n // a slice transitions to working at an unknown point in the future.\n // Instead we use @this.queuedSlices which really is a queue, whose elements are\n // dequeued in @this.matchSlicesWithSandboxes() and enqueued in fetchTask.\n // We should try to get rid of @this.slices, because it has low utility.\n // However, to minimize churn, this can be done later.\n //\n\n const availableSandboxes = this.unallocatedSpace;\n if (availableSandboxes <= 0) return;\n\n debugging() && console.log(`distributeQueuedSlices: unallocatedSpace ${availableSandboxes}, readied ${JSON.stringify(this.readiedSandboxes.map(s => s.id))}, queuedSlices ${this.queuedSlices.length}`);\n\n const sandboxSlices = await this.matchSlicesWithSandboxes(availableSandboxes);\n\n for (let sandboxSlice of sandboxSlices) {\n\n const { sandbox, slice } = sandboxSlice;\n if (sandbox) {\n debugging() && console.log(`${shortTime()}: distributeQueuedSlices: matched slice ${slice.sliceNumber} with sandbox ${sandbox.id} for job ${slice.jobAddress}, total sandbox count: ${this.sandboxes.length}`);\n if (sandbox.isReadyForAssign) {\n try {\n let timeoutMs = Math.floor(Math.min(+Supervisor.lastAssignFailTimerMs || 0, 10 * 60 * 1000 /* 10m */));\n await asleepMs(timeoutMs);\n if (sandbox.isReadyForAssign) { // Don't need this double-check anymore because of @this.matchSlicesWithSandboxes().\n sandbox.setIsAssigning(); // Don't need this either --> Set the state to assigning before the await to circumvent the event loop problem from await.\n await this.assignJobToSandbox(sandbox, slice.jobAddress);\n } else {\n return // Don't need this either --> The sandbox was picked a second time while it was being prepared, return without an error\n }\n } catch (e) {\n console.error(`Could not assign slice ${slice.sliceNumber} for job ${slice.jobAddress} to sandbox ${sandbox.id}`);\n if (Supervisor.debugBuild) console.error(`...exception`, e);\n Supervisor.lastAssignFailTimerMs = Supervisor.lastAssignFailTimerMs ? +Supervisor.lastAssignFailTimerMs * 1.25 : Math.random() * 200;\n this.returnSlice(slice);\n continue;\n }\n }\n\n if (!Supervisor.lastAssignFailTimerMs)\n Supervisor.lastAssignFailTimerMs = Math.random() * 200;\n this.startSandboxWork(sandbox, slice);\n Supervisor.lastAssignFailTimerMs = false;\n\n } else {\n // We should never get here.\n console.error(\"Supervisor.distributeQueuedSlices: Failed to find sandbox for slice\", {\n jobAddress: slice.jobAddress,\n sliceNumber: slice.sliceNumber\n });\n }\n }\n }\n\n /**\n *\n * @param {Sandbox} sandbox\n * @param {opaqueId} jobAddress\n * @returns {Promise<void>}\n */\n async assignJobToSandbox(sandbox, jobAddress) {\n var ceci = this;\n\n try {\n return sandbox.assign(jobAddress)\n } catch(error) {\n // return slice to scheduler, log error\n console.error('Supervisor.assignJobToSandbox: Failed to assign job to sandbox.', {\n jobAddress: jobAddress.substr(0,10),\n error,\n });\n\n await ceci.returnSandbox(sandbox);\n\n throw error;\n }\n }\n \n /**\n * Gives a slice to a sandbox which begins working. Handles collecting\n * the slice result (complete/fail) from the sandbox and submitting the result to the scheduler.\n * It will also return the sandbox to @this.returnSandbox when completed so the sandbox can be re-assigned.\n *\n * @param {Sandbox} sandbox - the sandbox to give the slice\n * @param {Slice} slice - the slice to distribute\n * @returns {Promise<void>} Promise returned from sandbox.run\n */\n async startSandboxWork (sandbox, slice) {\n var startDelayMs;\n\n try {\n slice.markAsWorking();\n } catch (e) {\n // This will occur when the same slice is distributed twice.\n // It is normal because two sandboxes could finish at the same time and be assigned the\n // same slice before the slice is marked as working.\n debugging() && console.debug(e);\n return;\n }\n\n // sandbox.requiresGPU = slice.requiresGPU;\n // if (sandbox.requiresGPU) {\n // this.GPUsAssigned++;\n // }\n\n if (Supervisor.startSandboxWork_beenCalled)\n startDelayMs = 1000 * (tuning.minSandboxStartDelay + (Math.random() * (tuning.maxSandboxStartDelay - tuning.minSandboxStartDelay)));\n else {\n startDelayMs = 1000 * tuning.minSandboxStartDelay;\n Supervisor.startSandboxWork_beenCalled = true;\n }\n \n try {\n debugging() && console.log(`${shortTime()} startSandboxWork: started sandbox.id.sliceNumber ${sandbox.id}.${slice.sliceNumber}.${slice.jobAddress}, total sandbox count: ${this.sandboxes.length}`);\n let result = await sandbox.work(slice, startDelayMs);\n slice.collectResult(result, true);\n // In watchdog, all sandboxes in 'working' state, have their status sent to result submitter.\n // However, this can happen after the sandbox/slice has already sent results\n // to result submitter, in which case, the activeSlices table has already removed the row\n // corresponding to slice and hence is incapable of updating status.\n sandbox.changeWorkingToAssigned();\n sandbox.allocated = false; // This is a little redundant: don't await any promises between here and the finally clause.\n this.assignedSandboxes.push(sandbox);\n debugging() && console.log(`${shortTime()} startSandboxWork: completed sandbox.id.sliceNumber ${sandbox.id}.${slice.sliceNumber}.${slice.jobAddress}, total sandbox count: ${this.sandboxes.length}`);\n } catch(error) {\n let logLevel;\n if (error instanceof SandboxError) {\n logLevel = 'warn';\n // The message and stack properties of error objects are not enumerable,\n // so they have to be copied into a plain object this way\n const errorResult = Object.getOwnPropertyNames(error).reduce((o, p) => {\n o[p] = error[p]; return o;\n }, { message: 'Unexpected worker error' });\n slice.collectResult(errorResult, false);\n } else {\n logLevel = 'error';\n // This error was unrelated to the work being done,\n // so just return the slice in the finally block.\n }\n\n // sandbox.public.name is defined in Sandbox.assign.\n // It is possible to get here with !sandbox.public.\n const jobName = sandbox.public ? sandbox.public.name : 'unnamed';\n const errorObject = {\n jobAddress: slice.jobAddress.substr(0,10),\n sliceNumber: slice.sliceNumber,\n sandbox: sandbox.id,\n jobName: jobName,\n };\n\n // XXXpfr Enabled Informative sandbox errors when in debug mode.\n if (!Supervisor.debugBuild && error.errorCode === 'EUNCAUGHTERROR') {\n console[logLevel](`Supervisor.startSandboxWork: Uncaught error in sandbox, could not compute.`, errorObject);\n } else {\n errorObject.stack = error.stack;\n console[logLevel](`Supervisor.startSandboxWork: Sandbox failed (${error.message})`, errorObject);\n }\n } finally {\n sandbox.allocated = false;\n if (slice.result) {\n await this.recordResult(slice);\n } else if (slice.error) {\n this.returnSlice(slice);\n } else {\n this.returnSlice(slice);\n }\n await this.returnSandbox(sandbox, slice);\n }\n }\n\n /**\n * Terminates sandboxes and returns slices.\n * Sets the working flag to false, call @this.work to start working again.\n * \n * If forceTerminate is true: Terminates all sandboxes and returns all slices.\n * If forceTerminate is false: Terminates non-working sandboxes and returns queued slices.\n *\n * @param {boolean} [forceTerminate = true] - true if you want to stop the sandboxes from completing their current slice.\n * @returns {Promise<void>}\n */\n async stopWork (forceTerminate = true) {\n if (forceTerminate) {\n let sandbox;\n while (sandbox = this.sandboxes.pop()) {\n sandbox.terminate(false);\n }\n\n await this.returnSlices(this.slices).then(() => {\n this.slices.length = 0;\n this.queuedSlices.length = 0;\n });\n\n this.resultSubmitterConnection.off('close', this.openResultSubmitterConn);\n this.resultSubmitterConnection.close();\n this.resultSubmitterConnection = null;\n } else {\n // Only terminate idle sandboxes and return only queued slices\n let idleSandboxes = this.sandboxes.filter(w => !w.isWorking);\n for (let sandbox of idleSandboxes) {\n sandbox.terminate(false);\n }\n\n let queuedSlices = this.queuedSlices;\n await this.returnSlices(queuedSlices).then(() => {\n // Kill corresponding entries in this.slices .\n this.queuedSlices.forEach(slice => {\n this.removeElement(this.slices, slice);\n });\n this.queuedSlices.length = 0;\n });\n await new Promise((resolve, reject) => {\n let sandboxesRemaining = this.workingSandboxes.length;\n if (sandboxesRemaining === 0)\n {\n resolve();\n }\n // Resolve and finish work once all sandboxes have finished submitting their results.\n this.on('submitFinished', () => {\n sandboxesRemaining--;\n if (sandboxesRemaining === 0)\n {\n console.log('All sandboxes empty, stopping worker and closing all connections');\n resolve();\n }\n });\n })\n this.resultSubmitterConnection.off('close', this.openResultSubmitterConn);\n this.resultSubmitterConnection.close();\n this.resultSubmitterConnection = null;\n }\n\n this.taskDistributorConnection.off('close', this.openTaskDistributorConn);\n this.taskDistributorConnection.close();\n this.taskDistributorConnection = null;\n\n this.packageManagerConnection.off('close', this.openPackageManagerConn);\n this.packageManagerConnection.close();\n this.packageManagerConnection = null;\n\n this.eventRouterConnection.off('close', this.openEventRouterConn);\n this.eventRouterConnection.close();\n this.eventRouterConnection = null;\n\n this.emit('stop');\n }\n\n /**\n * Takes a slice and returns it to the scheduler to be redistributed.\n * Usually called when the supervisor told it to forcibly stop working.\n *\n * @param {Slice} slice - the slice to return to the scheduler\n * @returns {Promise<*>} - response from the scheduler the slice was returned to\n */\n returnSlice (slice) {\n // Remove the slice from the slices array.\n this.removeElement(this.slices, slice);\n\n const jobAddress = slice.jobAddress;\n const sliceNumber = slice.sliceNumber;\n const authorizationMessage = this.checkAuthorization('Supervisor.returnSlice:', jobAddress, sliceNumber);\n if (!authorizationMessage) return undefined;\n\n let payload = {};\n if (slice.error) {\n payload = slice.getReturnMessagePayload(this.workerOpaqueId, authorizationMessage, 'uncaught');\n } else {\n payload = slice.getReturnMessagePayload(this.workerOpaqueId, authorizationMessage);\n }\n\n return this.resultSubmitterConnection.send('status', payload)\n .then(response => {\n return response;\n })\n .catch(error => {\n console.error('Failed to return slice', {\n sliceIdentifier: slice.identifier,\n sliceNumber: sliceNumber,\n jobAddress: jobAddress,\n error,\n });\n })\n }\n\n /** Bulk-return multiple slices, possibly for assorted jobs\n */\n returnSlices(slices) {\n const slicePayload = [];\n\n slices.forEach(slice => {\n addToSlicePayload(slicePayload, slice.jobAddress, slice.sliceNumber, 'return', this.checkAuthorization('Supervisor.returnSlices:', slice.jobAddress, slice.sliceNumber));\n });\n\n return this.resultSubmitterConnection.send('status', {\n worker: this.workerOpaqueId,\n slices: slicePayload,\n });\n }\n\n /**\n * Submits the slice results to the scheduler, either to the\n * work submit or fail endpoints based on the slice status.\n * Then remove the slice from the @this.slices cache.\n *\n * @param {Slice} slice - the slice to submit\n * @returns {Promise<void>}\n */\n async recordResult (slice) {\n const metrics = { GPUTime: 0, CPUTime: 0, CPUDensity: 0, GPUDensity: 0 }; \n \n debugging('supervisor') && console.log('supervisor: recording result');\n assert(slice && slice.hasOwnProperty('result'), slice.sliceNumber, slice.jobAddress);\n\n const jobAddress = slice.jobAddress;\n const sliceNumber = slice.sliceNumber;\n const authorizationMessage = this.checkAuthorization('Supervisor.recordResult:', jobAddress, sliceNumber);\n if (!authorizationMessage) return; // Question: Is it better to proceed and have a cascade of errors?\n\n /* @see result-submitter::result for full message details */\n var payloadData = {\n slice: sliceNumber,\n job: jobAddress,\n worker: this.workerOpaqueId,\n paymentAddress: this.paymentAddress,\n metrics,\n authorizationMessage,\n }\n const isValidTimeReport = slice.result && slice.result.timeReport && slice.result.timeReport.total > 0;\n if (isValidTimeReport) {\n const timeReport = slice.result.timeReport;\n assert(timeReport.total);\n metrics.GPUTime = timeReport.webGL;\n metrics.CPUTime = timeReport.total - (metrics.GPUTime + timeReport.idle);\n metrics.CPUDensity = metrics.CPUTime / timeReport.total;\n metrics.GPUDensity = metrics.GPUTime / timeReport.total;\n }\n \n this.emit('submittingResult');\n\n if (!slice.isFinished)\n throw new Error('Cannot record result for slice that is not finished');\n\n if (slice.resultStorageDetails === 'pattern') /* this is a remote-storage slice */\n payloadData.result = await sendResultToRemote(slice);\n else {\n // It is possible for slice.result to be undefined when there are upstream errors.\n if (!slice.result)\n throw new Error('slice.result is undefined. This is ok when there are upstream errors.');\n payloadData.result = encodeDataURI(slice.result.result); /* XXXwg - result.result is awful */\n }\n\n try {\n if (slice.completed) {\n\n /* work function returned a result */\n const { success, payload } = await this.resultSubmitterConnection.send(\n 'result',\n payloadData,\n );\n\n if (!success) {\n throw new DCPError('Unable to submit result for work done', payload);\n }\n\n const receipt = {\n accepted: true,\n payment: payload.slicePaymentAmount,\n };\n this.emit('submittedResult', payload);\n this.emit('dccCredit', receipt);\n } else {\n\n /* slice did not complete for some reason */\n let statusPayloadData = {\n worker: this.workerOpaqueId,\n slices: [\n {\n sliceNumber: sliceNumber,\n job: jobAddress,\n status: 'return', // special state looked for in status.js / result-submitter\n reason: 'uncaught', // special state looked for in status.js / result-submitter\n /** @todo XXXpfr: Is there error info we can use here? */ \n // error: slice.error, \n authorizationMessage,\n }\n ], \n };\n \n await this.resultSubmitterConnection.send('status', statusPayloadData);\n }\n } catch(error) {\n console.info(`1014: Failed to submit results for slice ${payloadData.slice} of job ${payloadData.job}`, error);\n this.emit('submitSliceFailed', error);\n }\n finally\n {\n this.emit('submitFinished');\n // Remove the slice from the slices array\n this.removeElement(this.slices, slice);\n }\n }\n}\n\n/** \n * Send a work function's result to a server that speaks our DCP Storage Server protocol.\n * A sample storage server has been implemented in https://gitlab.com/Distributed-Compute-Protocol/dcp-storage-server\n * as part of DCP-1479.\n *\n * @param {Slice} slice slice object whose result we are sending\n * @returns {Promise<object>} object with properties contentType and uri\n * @throws if the remote server returned a non-ok status object, or an HTTP status not in the 2xx range.\n */\nasync function sendResultToRemote(slice) {\n const postParams = {\n ...JSON.parse(slice.resultStorageParams), // These come in as a String from the db currently, @todo - output as an object\n };\n\n const sliceResultUri = makeSliceURI('pattern', slice.resultStorageDetails, {\n slice: slice.sliceNumber,\n job: slice.jobAddress,\n //juuid: slice.job.uuid -- what should be here?\n });\n\n const url = new DcpURL(sliceResultUri);\n\n if (dcpConfig.worker.allowOrigins.any.indexOf(url.origin) === -1 &&\n dcpConfig.worker.allowOriginResultStorage.indexOf(url.origin) === -1) {\n throw new Error(`Invalid origin for remote result storage: '${url.origin}'`);\n }\n \n postParams.slice = slice.sliceNumber;\n postParams.job = slice.jobAddress;\n postParams.contentType = 'application/json'; // Currently data will be outputed as a JSON object, @todo: Support file upload\n\n if (slice.result.result)\n postParams.result = slice.result.result;\n else\n postParams.error = new Error('no result'); /** @todo XXXwg return the error object from the sandbox */\n\n const formBodyArray = [];\n for (const property in postParams) {\n const encodedKey = encodeURIComponent(property);\n const encodedValue = encodeURIComponent(postParams[property]);\n formBodyArray.push(`${encodedKey}=${encodedValue}`);\n }\n const formBody = formBodyArray.join('&');\n\n let postPromise = new Promise(function supervisorPostResult(resolve, reject) {\n let deeperErrorStack = new Error().stack;\n deeperErrorStack = deeperErrorStack.substring(deeperErrorStack.indexOf('\\n') + 1);\n const xhr = new XMLHttpRequest();\n xhr.onloadend = function supervisor$$recordResult$onloadend() {\n try {\n let o;\n \n delete xhr.onloadend;\n if (xhr.status >= 200 && xhr.status < 300) {\n /* Successful post: record the resultant URL; needed for dcp-client application to be able to fetch the result */\n switch(xhr.getResponseHeader('content-type'))\n {\n default: /* support lousy web servers */\n case 'application/json': {\n o = JSON.parse(xhr.responseText);\n break;\n }\n case 'application/x-kvin': {\n o = __webpack_require__(/*! kvin */ \"./node_modules/kvin/kvin.js\").deserialize(xhr.responseText);\n break;\n }\n }\n\n if (o.status === 'ok')\n return resolve(o);\n \n return reject(new Error(`Remote server ${url} did not set status=ok (o.error ? ${JSON.stringify(o)} : error)`));\n } else {\n const error = new Error(`HTTP Error ${xhr.status} sending slice results to ${url}`);\n //\n // Note: This impl is mostly grafted from justFetch in dcp/src/protocol-v3/index.\n // It is a safe assumption that this function has never been tested.\n // Refine the following error info when we support remote-storage.\n //\n error.request = xhr;\n error.request.location = url;\n error.request.method = 'POST';\n error.request.status = xhr.status;\n error.stack += '\\n----------\\n' + deeperErrorStack;\n throw error;\n }\n } catch (e) {\n return reject(e);\n }\n\n return reject(new Error('impossible'));\n }\n \n xhr.onreadystatechange = function() {\n if (this.readyState == 4 && this.status == 200) {\n debugging('supervisor') && console.log('Response text', this.responseText);\n }\n }\n \n xhr.open('POST', url.href, true);\n xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');\n xhr.send(formBody);\n });\n\n return postPromise;\n}\n\n/**\n * Return DCPv4-specific connection options, composed of type-specific, URL-specific, \n * and worker-specific options, any/all of which can override the dcpConfig.dcp.connectOptions.\n * The order of precedence is the order of specificity.\n */\nfunction connectionOptions(url, label) {\n return leafMerge(/* ordered from most to least specific */\n dcpConfig.worker.dcp.connectionOptions.default,\n dcpConfig.worker.dcp.connectionOptions[label],\n dcpConfig.worker.dcp.connectionOptions[url.href]);\n}\n\n\n/**\n * Add a slice to the slice payload being built. If a sliceList already exists for the \n * job-status-authMessage tuple, then the slice will be added to that, otherwise a new \n * sliceList will be added to the payload.\n *\n * @param {Object[]} slicePayload Slice payload being built. Will be mutated in place.\n * @param {Address} job Address of job the slice belongs to\n * @param {Number} sliceNumber Slice number\n * @param {String} status Status update, eg. progress or scheduled\n * @param {Object} authorizationMessage authorizationMessage for the slice\n *\n * @returns {Object[]} mutated slicePayload array\n */\nfunction addToSlicePayload(slicePayload, job, sliceNumber, status, authorizationMessage) {\n // Try to find a sliceList in the payload which matches the job, status, and auth message\n let sliceList = slicePayload.find(desc => {\n return desc.job === job && desc.status === status && desc.authorizationMessage === authorizationMessage;\n });\n\n // If we didn't find a sliceList, start a new one and add it to the payload\n if (!sliceList) {\n sliceList = {\n job,\n sliceNumbers: [],\n status,\n authorizationMessage: authorizationMessage,\n };\n slicePayload.push(sliceList);\n }\n\n sliceList.sliceNumbers.push(sliceNumber);\n\n return slicePayload;\n}\n\n/** @type {number | boolean} */\nSupervisor.lastAssignFailTimerMs = false;\n/** @type {boolean} */\nSupervisor.startSandboxWork_beenCalled = false;\n/** @type {boolean} */\nSupervisor.debugBuild = (__webpack_require__(/*! dcp/common/dcp-build */ \"./src/common/dcp-build.js\").build === 'debug');\n\nexports.Supervisor = Supervisor;\n\n\n//# sourceURL=webpack:///./src/dcp-client/worker/supervisor.js?");
5406
+ eval("/**\n * @file worker/supervisor.js\n *\n * The component that controls each of the sandboxes\n * and distributes work to them. Also communicates with the\n * scheduler to fetch said work.\n *\n * The supervisor readies sandboxes before/while fetching slices.\n * This means sometimes there are extra instantiated WebWorkers\n * that are idle (in this.readiedSandboxes). Readied sandboxes can\n * be used for any slice. After a readied sandbox is given a slice\n * it becomes assigned to slice's job and can only do work\n * for that job.\n *\n * After a sandbox completes its work, the sandbox becomes cached\n * and can be reused if another slice with a matching job is fetched.\n *\n * @author Matthew Palma, mpalma@kingsds.network\n * Ryan Rossiter, ryan@kingsds.network\n * @date May 2019\n */\n\n/* global dcpConfig */\n// @ts-check\n\n\nconst nanoid = __webpack_require__(/*! nanoid/non-secure */ \"./node_modules/nanoid/non-secure/index.js\");\nconst constants = __webpack_require__(/*! dcp/common/scheduler-constants */ \"./src/common/scheduler-constants.js\");\nconst hash = __webpack_require__(/*! dcp/common/hash */ \"./src/common/hash.js\");\nconst wallet = __webpack_require__(/*! dcp/dcp-client/wallet */ \"./src/dcp-client/wallet/index.js\");\nconst protocolV4 = __webpack_require__(/*! dcp/protocol-v4 */ \"./src/protocol-v4/index.js\");\nconst DCP_ENV = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\n\nconst debugging = __webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope('worker');\nconst { assert } = __webpack_require__(/*! dcp/common/dcp-assert */ \"./src/common/dcp-assert.js\");\nconst { EventEmitter } = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\");\nconst { Sandbox, SandboxError } = __webpack_require__(/*! ./sandbox */ \"./src/dcp-client/worker/sandbox.js\");\nconst { Slice } = __webpack_require__(/*! ./slice */ \"./src/dcp-client/worker/slice.js\");\nconst { SupervisorCache } = __webpack_require__(/*! ./supervisor-cache */ \"./src/dcp-client/worker/supervisor-cache.js\");\nconst { DcpURL } = __webpack_require__(/*! dcp/common/dcp-url */ \"./src/common/dcp-url.js\");\nconst { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\nconst { localStorage } = __webpack_require__(/*! dcp/common/dcp-localstorage */ \"./src/common/dcp-localstorage.js\");\nconst { booley, encodeDataURI, makeSliceURI, leafMerge, asleepMs, shortTime,\n dumpSandboxes, dumpSlices, dumpSandboxesIfNotUnique, dumpSlicesIfNotUnique } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\nconst { calculateJoinHash } = __webpack_require__(/*! dcp/dcp-client/compute-groups */ \"./src/dcp-client/compute-groups/index.js\");\nconst debug = (...args) => {\n if (debugging('supervisor')) {\n console.debug('dcp-client:worker:supervisor', ...args);\n }\n};\n\nconst supervisorTuning = dcpConfig.future('supervisor.tuning');\nconst tuning = {\n watchdogInterval: 7, /**< seconds - time between fetches when ENOTASK(? /wg nov 2019) */\n minSandboxStartDelay: 0.1, /**< seconds - minimum time between WebWorker starts */\n maxSandboxStartDelay: 0.7, /**< seconds - maximum delay time between WebWorker starts */\n ...supervisorTuning\n};\n\n/** Make timers 10x slower when running in niim */\nlet timeDilation = 1;\nif (DCP_ENV.platform === 'nodejs') {\n /** Make timers 10x slower when running in niim */\n timeDilation = (requireNative('module')._cache.niim instanceof requireNative('module').Module) ? 10 : 1;\n}\n\ndcpConfig.future('worker.sandbox', { progressReportInterval: (5 * 60 * 1000) });\nconst sandboxTuning = dcpConfig.worker.sandbox;\n\n/**\n * @typedef {*} opaqueId\n */\n\n/**\n * @typedef {object} SandboxSlice\n * @property {Sandbox} sandbox\n * @property {Slice} slice\n */\n\n/**\n * @typedef {object} Signature\n * @property {Uint8Array} r\n * @property {Uint8Array} s\n * @property {Uint8Array} v\n */\n\n/**\n * @typedef {object} SignedAuthorizationMessageObject\n * @property {object} auth\n * @property {Signature} signature\n * @property {module:dcp/wallet.Address} owner\n */\n\n/** @typedef {import('.').Worker} Worker */\n/** @typedef {import('.').SupervisorOptions} SupervisorOptions */\n\nclass Supervisor extends EventEmitter {\n /**\n * @constructor\n * @param {Worker} worker\n * @param {SupervisorOptions} options\n */\n constructor (worker, options={}) {\n super('Supervisor');\n\n /** @type {Worker} */\n this.worker = worker;\n\n /** @type {Sandbox[]} */\n this.sandboxes = [];\n\n /**\n * XXXpfr @todo We may still needs this.slices to communicate state to result-submitter status endpoint, but it isn't used otherwise.\n * @deprecated\n * @type {Slice[]}\n */\n this.slices = [];\n\n /**\n * The true type is associative array of associative array of SignedAuthorizationMessageObject.\n * How can that be specified in jsdoc?\n * @type {object}\n */\n this.authorizationMessages = {};\n\n /** @type {Slice[]} */\n this.queuedSlices = [];\n\n /** @type {Sandbox[]} */\n this.readiedSandboxes = [];\n\n /** @type {Sandbox[]} */\n this.assignedSandboxes = [];\n\n /** @type {boolean} */\n this.matching = false;\n\n /** @type {boolean} */\n this.doNotPrune = false;\n\n if (!options) {\n console.error('Supervisor Options', options, new Error().stack);\n options = {};\n }\n \n /** @type {object} */\n this.options = {\n jobAddresses: options.jobAddresses || [/* all jobs unless priorityOnly */],\n ...options,\n };\n\n const { paymentAddress, identityKeystore } = options;\n if (paymentAddress) {\n if (paymentAddress instanceof wallet.Keystore) {\n this.paymentAddress = paymentAddress.address;\n } else {\n this.paymentAddress = new wallet.Address(paymentAddress);\n }\n } else {\n this.paymentAddress = null;\n }\n\n this._identityKeystore = identityKeystore;\n\n this.allowedOrigins = dcpConfig.worker.allowOrigins.any;\n if(options.allowedOrigins && options.allowedOrigins.length > 0)\n this.allowedOrigins = options.allowedOrigins.concat(this.allowedOrigins);\n\n /**\n * Maximum sandboxes allowed to work at a given time.\n * @type {number}\n */\n this.maxWorkingSandboxes = options.maxWorkingSandboxes || 1;\n\n /** @type {number} */\n this.defaultMaxGPUs = 1;\n // this.GPUsAssigned = 0;\n \n // Object.defineProperty(this, 'GPUsAssigned', {\n // get: () => this.workingSandboxes.filter(sb => !!sb.requiresGPU).length,\n // enumerable: true,\n // configurable: false,\n // });\n\n /** @type {boolean} */\n this.isFetchingNewWork = false;\n /**\n * TODO: Remove this when the supervisor sends all of the sandbox\n * capabilities to the scheduler when fetching work.\n * @type {object}\n */\n this.capabilities = null;\n\n // This sets an offset into the watchdog bin at which to fire the sweeper\n /** @deprecated - UNUSED */\n this.watchdogSlotTime = options.watchdogInterval || tuning.watchdogInterval * 1000;\n /** @deprecated - UNUSED */\n this.watchdogOffset = Math.random();\n /** @deprecated - UNUSED */\n this.watchdogTimeout = null;\n\n /** @type {number} */\n this.lastProgressReport = 0;\n\n // @hack - dcp-env.isBrowserPlatform is not set unless the platform is _explicitly_ set,\n // using the default detected platform doesn't set it.\n // Fixing that causes an error in the wallet module's startup on web platform, which I\n // probably can't fix in a reasonable time this morning.\n // ~ER2020-02-20\n\n if (!options.maxWorkingSandboxes\n && DCP_ENV.browserPlatformList.includes(DCP_ENV.platform)\n && navigator.hardwareConcurrency > 1) {\n this.maxWorkingSandboxes = navigator.hardwareConcurrency - 1;\n if (typeof navigator.userAgent === 'string') {\n if (/(Android).*(Chrome|Chromium)/.exec(navigator.userAgent)) {\n this.maxWorkingSandboxes = 1;\n console.log('Doing work with Chromimum browsers on Android is currently limited to one sandbox');\n }\n }\n }\n\n /** @type {SupervisorCache} */\n this.cache = new SupervisorCache(this);\n /** @type {object} */\n this._connections = {}; /* active DCPv4 connections */\n // Call the watchdog every 7 seconds.\n this.watchdogInterval = setInterval(() => this.watchdog(), 7000);\n\n const ceci = this;\n\n // Initialize to null so these properties are recognized for the Supervisor class\n this.taskDistributorConnection = null;\n this.eventRouterConnection = null;\n this.resultSubmitterConnection = null;\n this.packageManagerConnection = null;\n this.openTaskDistributorConn = function openTaskDistributorConn()\n {\n let config = dcpConfig.scheduler.services.taskDistributor;\n ceci.taskDistributorConnection = new protocolV4.Connection(config, ceci.identityKeystore, connectionOptions(config.location, 'taskDistributor'));\n ceci.taskDistributorConnection.on('close', ceci.openTaskDistributorConn);\n }\n\n this.openEventRouterConn = function openEventRouterConn()\n {\n let config = dcpConfig.scheduler.services.eventRouter;\n ceci.eventRouterConnection = new protocolV4.Connection(config, ceci.identityKeystore, connectionOptions(config.location, 'eventRouter'));\n ceci.eventRouterConnection.on('close', ceci.openEventRouterConn);\n }\n \n this.openResultSubmitterConn = function openResultSubmitterConn()\n {\n let config = dcpConfig.scheduler.services.resultSubmitter;\n ceci.resultSubmitterConnection = new protocolV4.Connection(config, ceci.identityKeystore, connectionOptions(config.location, 'resultSubmitter'));\n ceci.resultSubmitterConnection.on('close', ceci.openResultSubmitterConn);\n }\n\n this.openPackageManagerConn = function openPackageManagerConn()\n {\n let config = dcpConfig.packageManager;\n ceci.packageManagerConnection = new protocolV4.Connection(config, ceci.identityKeystore, connectionOptions(config.location, 'packageManager'));\n ceci.packageManagerConnection.on('close', ceci.openPackageManagerConn);\n }\n }\n\n /**\n * Return worker opaqueId.\n * @type {opaqueId}\n */\n get workerOpaqueId() {\n if (!this._workerOpaqueId)\n this._workerOpaqueId = localStorage.getItem('workerOpaqueId');\n\n if (!this._workerOpaqueId || this._workerOpaqueId.length !== 22) { /** @todo XXXwg fix magic number problem */\n this._workerOpaqueId = nanoid(22);\n localStorage.setItem('workerOpaqueId', this._workerOpaqueId);\n }\n\n return this._workerOpaqueId;\n }\n\n /**\n * This getter is the absolute source-of-truth for what the\n * identity keystore is for this instance of the Supervisor.\n */\n get identityKeystore() {\n assert(this.defaultIdentityKeystore);\n\n return this._identityKeystore || this.defaultIdentityKeystore;\n }\n\n /**\n * Open all connections. Used when supervisor is instantiated or stopped/started\n * to initially open connections.\n */\n instantiateAllConnections() {\n this.openTaskDistributorConn();\n this.openEventRouterConn();\n this.openResultSubmitterConn();\n this.openPackageManagerConn();\n }\n\n /** Set the default identity keystore -- needs to happen before anything that talks\n * to the scheduler for work gets called. This is a wart and should be removed by\n * refactoring.\n *\n * The default identity keystore will be used if the Supervisor was not provided\n * with an alternate. This keystore will be located via the Wallet API, and \n * if not found, a randomized default identity will be generated. \n *\n * @param {object} ks An instance of wallet::Keystore -- if undefined, we pick the best default we can.\n * @returns {Promise<void>}\n */\n async setDefaultIdentityKeystore(ks) {\n try {\n if (ks) {\n this.defaultIdentityKeystore = ks;\n return;\n }\n\n if (this.defaultIdentityKeystore)\n return;\n\n try {\n this.defaultIdentityKeystore = await wallet.getId();\n } catch(e) {\n debugging('supervisor') && console.debug('supervisor: generating default identity', this.defaultIdentityKeystore.address);\n this.defaultIdentityKeystore = await new wallet.IdKeystore(null, '');\n }\n } finally {\n debugging('supervisor') && console.debug('supervisor: set default identity =', this.defaultIdentityKeystore.address);\n }\n }\n \n //\n // What follows is a bunch of utility functions for creating filtered views\n // of the slices and sandboxes array.\n //\n\n /**\n * Sandboxes that are in WORKING state.\n * Warning: Do not rely on this information being 100% accurate -- it may change in the next instant.\n * @type {Sandbox[]}\n */\n get workingSandboxes() {\n return this.sandboxes.filter(sandbox => sandbox.isWorking);\n }\n\n /**\n * This property is used as the target number of sandboxes to be associated with slices and transition sandbox-state from ASSIGNED -> WORKING.\n *\n * It is used in this.watchdog as to prevent a call to this.work when unallocatedSpace <= 0.\n * It is also used in this.distributeQueuedSlices where it is passed as an argument to this.matchSlicesWithSandboxes to indicate how many sandboxes\n * to associate with slices and transition sandbox-state from ASSIGNED -> WORKING.\n *\n * Warning: Do not rely on this information being 100% accurate -- it may change in the next instant.\n * @type {number}\n */\n get unallocatedSpace() {\n return this.maxWorkingSandboxes - this.sandboxes.filter(sandbox => sandbox.allocated).length;\n }\n\n /**\n * Slices that have SLICE_STATUS_UNASSIGNED status.\n * Warning: Do not rely on this information being 100% accurate -- it may change in the next instant.\n * @type {Slice[]}\n */\n get snapshotOfQueuedSlices() {\n return this.slices.filter(slice => slice.isUnassigned);\n }\n\n /**\n * Slices that have SLICE_STATUS_WORKING status.\n * Warning: Do not rely on this information being 100% accurate -- it may change in the next instant.\n * @type {Slice[]}\n */\n get snapshotOfWorkingSlices() {\n return this.slices.filter(slice => slice.isWorking)\n }\n\n /**\n * Sandboxes that are in READY_FOR_ASSIGN state.\n * Warning: Do not rely on this information being 100% accurate -- it may change in the next instant.\n * @type {Sandbox[]}\n */\n get snapshotOfReadiedSandboxes() {\n return this.sandboxes.filter(sandbox => sandbox.isReadyForAssign);\n }\n\n /**\n * Sandboxes that are in ASSIGNED state.\n * Warning: Do not rely on this information being 100% accurate -- it may change in the next instant.\n * @type {Sandbox[]}\n */\n get snapshotOfAssignedSandboxes() {\n return this.sandboxes.filter(sandbox => sandbox.isAssigned);\n }\n\n /**\n * Sandboxes that are in TERMINATED state.\n * Warning: Do not rely on this information being 100% accurate -- it may change in the next instant.\n * @type {Sandbox[]}\n */\n get snapshotOfTerminatedSandboxes() {\n return this.sandboxes.filter(sandbox => sandbox.isTerminated);\n }\n\n /**\n * Remove element from theArray.\n * @parameter {Array<*>} theArray\n * @parameter {object|number} element\n * @parameter {boolean} [assertExists = true]\n */\n removeElement(theArray, element, assertExists = false) {\n let index = theArray.indexOf(element);\n assert(index !== -1 || !assertExists);\n if (index !== -1) theArray.splice(index, 1);\n }\n\n /**\n * Check authorization message wrt jobAddress and sliceNumber.\n * @parameter {string} text\n * @parameter {opaqueId} jobAddress\n * @parameter {number} sliceNumber\n * @returns {SignedAuthorizationMessageObject}\n */\n checkAuthorization(text, jobAddress, sliceNumber) {\n const authorizationMessage = this.authorizationMessages[jobAddress][sliceNumber];\n if (!authorizationMessage)\n console.log(`\\t${text} DANGER: Undefined authorization. ${jobAddress}, ${sliceNumber}`);\n return authorizationMessage;\n }\n\n /**\n * If the elements of sandboxSliceArray are not unique, log the duplicates and dump the array.\n * @parameter {SandboxSlice[]} sandboxSliceArray\n * @parameter {string} header\n */\n dumpSandboxSlicesIfNotUnique(sandboxSliceArray, header) {\n if (!this.isUniqueSandboxSlices(sandboxSliceArray, header))\n this.dumpSandboxSlices(sandboxSliceArray);\n }\n\n /**\n * Log sandboxSlice.\n * @parameter {SandboxSlice} sandboxSlice\n * @returns {string}\n */\n dumpSandboxSlice(sandboxSlice) {\n return `${sandboxSlice.sandbox.id}-${sandboxSlice.slice.sliceNumber}.${sandboxSlice.slice.jobAddress}`;\n }\n\n /**\n * Log { sandbox, slice }.\n * @parameter {Sandbox} sandbox\n * @parameter {Slice} slice\n * @returns {string}\n */\n dumpSandboxAndSlice(sandbox, slice) {\n return this.dumpSandboxSlice({ sandbox, slice });\n }\n\n /**\n * Dump sandboxSliceArray.\n * @parameter {SandboxSlice[]} sandboxSliceArray\n * @parameter {string} header\n */\n dumpSandboxSlices(sandboxSliceArray, header) {\n if (header) console.log(`\\n${header}`);\n sandboxSliceArray.forEach(x => console.log(`\\t{ ${x.sandbox.id}.${x.sandbox.jobAddress}.${x.sandbox.state}, ${x.slice.sliceNumber}.${x.slice.jobAddress}.${x.slice.status} }`));\n }\n\n /**\n * Check sandboxSliceArray for duplicates.\n * @parameter {SandboxSlice[]} sandboxSliceArray\n * @parameter {string} header\n * @parameter {function} log\n * @returns {boolean}\n */\n isUniqueSandboxSlices(sandboxSliceArray, header, log) {\n return sandboxSliceArray.length === this.makeUniqueSandboxSlices(sandboxSliceArray, header, log).length;\n }\n\n /**\n * Returns a copy of sandboxSliceArray with all duplicates removed.\n * @parameter {SandboxSlice[]} sandboxSliceArray\n * @parameter {string} header\n * @parameter {function} log\n * @returns {SandboxSlice[]}\n */\n makeUniqueSandboxSlices(sandboxSliceArray, header, log) {\n const result = [], slices = [], sandboxes = [];\n let once = true;\n sandboxSliceArray.forEach(x => {\n const sliceIndex = slices.indexOf(x.slice);\n const sandboxIndex = sandboxes.indexOf(x.sandbox);\n\n if (sandboxIndex >= 0) {\n if (once && header) console.log(`\\n${header}`); once = false;\n log ? log(x.sandbox) : console.log(`\\tDANGER: Found duplicate sandbox ${x.sandbox.id}.${x.sandbox.jobAddress}.${x.sandbox.state}.`);\n } else sandboxes.push(x.sandbox);\n\n if (sliceIndex >= 0) {\n if (once && header) console.log(`\\n${header}`); once = false;\n log ? log(x.slice) : console.log(`\\tDANGER: Found duplicate slice ${x.slice.sliceNumber}.${x.slice.jobAddress}.${x.slice.status}.`);\n } else {\n slices.push(x.slice);\n if (sandboxIndex < 0) result.push(x);\n }\n });\n return result;\n }\n\n /** NOT used ATM, it's faster to just filter the assigned sandboxes for a jobAddress on-demand\n * \n * Bins the assigned sandboxes in an object, keyed by their jobAddress.\n * { 0xgenAddress1: sandboxes[], 0xgenAddress2: sandboxes[], ... }\n * @type {object}\n */\n get assignedSandboxesSorted () {\n return this.assignedSandboxes.reduce((o, w) => {\n if (!w.jobAddress) throw new Error(\"Assigned sandbox doesn't have a job opaque id\", w);\n\n if (w.jobAddress in o) {\n o[w.jobAddress].push(w);\n } else o[w.jobAddress] = [w];\n\n return o;\n }, {});\n }\n\n /**\n * Attempts to create and start a given number of sandboxes.\n * The sandboxes that are created can then be assigned for a\n * specific job at a later time. All created sandboxes\n * get put into the @this.readiedSandboxes array.\n *\n * @param {number} numSandboxes - the number of sandboxes to create\n * @returns {Promise<Sandbox[]>} - resolves with array of created sandboxes, rejects otherwise\n * @throws when given a numSandboxes is not a number > 0 or if numSandboxes is Infinity\n */\n async readySandboxes (numSandboxes) {\n debug('Readying sandboxes');\n if (typeof numSandboxes !== 'number' || Number.isNaN(numSandboxes) || numSandboxes === Infinity) {\n throw new Error(`${numSandboxes} is not a number of sandboxes that can be readied.`);\n }\n if (numSandboxes <= 0) {\n return [];\n }\n\n const sandboxStartPromises = [];\n const sandboxes = [];\n const errors = [];\n for (let i = 0; i < numSandboxes; i++) {\n const sandbox = new Sandbox(this.cache, {\n ...this.options.sandboxOptions,\n }, this.allowedOrigins);\n sandbox.addListener('ready', () => this.emit('sandboxReady', sandbox));\n sandbox.addListener('start', () => {\n this.emit('sandboxStart', sandbox);\n\n // When sliceNumber == 0, result-submitter status skips the slice,\n // so don't send it in the first place.\n // The 'start' event is fired when a worker starts up, hence there's no way\n // to determine whether sandbox has a valid slice without checking.\n if (sandbox.slice) {\n const jobAddress = sandbox.jobAddress;\n const sliceNumber = sandbox.slice.sliceNumber;\n const authorizationMessage = this.checkAuthorization('Supervisor.readySandboxes sandboxStart:', jobAddress, sliceNumber);\n if (!authorizationMessage) return;\n\n this.resultSubmitterConnection.send('status', {\n worker: this.workerOpaqueId,\n slices: [{\n job: jobAddress,\n sliceNumber: sliceNumber,\n status: 'begin',\n authorizationMessage,\n }],\n });\n }\n });\n sandbox.addListener('workEmit', ({ eventName, payload }) => {\n // Need to check if the sandbox hasn't been assigned a slice yet.\n if (!sandbox.slice) {\n if (Supervisor.debugBuild) {\n console.error(\n `Sandbox not assigned a slice before sending workEmit message to scheduler. 'workEmit' event originates from \"${eventName}\" event`, \n payload,\n );\n }\n } else { // Need else otherwise assert fires.\n\n const jobAddress = sandbox.slice.jobAddress;\n const sliceNumber = sandbox.slice.sliceNumber;\n // sliceNumber can be zero if it came from a problem with loading modules.\n assert(jobAddress && (sliceNumber || sliceNumber === 0));\n try {\n /* Make sure we actually have an authorization message at jobAddress and sliceNumber. */\n const authorizationMessage = this.checkAuthorization('Supervisor.readySandboxes workEmit:', jobAddress, sliceNumber);\n if (!authorizationMessage) return;\n\n /* DCP-1698 Send auth msg with tasks to worker, then validate authority of worker to send slice info back to scheduler. */\n this.eventRouterConnection.send('workEmit', {\n eventName,\n payload,\n job: jobAddress,\n slice: sliceNumber,\n worker: this.workerOpaqueId,\n authorizationMessage,\n });\n } catch (error) {\n debugging('supervisor') &&\n console.debug(\n `Authorizing failed: jobAddress: ${jobAddress}, slice: ${sliceNumber}, error: ${error}`,\n );\n }\n }\n });\n\n // When any sbx completes, \n sandbox.addListener('complete', () => {\n // this.distributeQueuedSlices();\n this.watchdog();\n //this.work();\n });\n\n const delayMs =\n 1000 *\n (tuning.minSandboxStartDelay +\n Math.random() *\n (tuning.maxSandboxStartDelay - tuning.minSandboxStartDelay));\n \n sandboxStartPromises.push(\n sandbox\n .start(delayMs)\n .then(() => {\n this.sandboxes.push(sandbox);\n })\n .catch((err) => {\n errors.push(err);\n if(err.code === 'ENOWORKER'){\n throw new DCPError(\"Cannot use localExec without dcp-worker installed. Use the command 'npm install dcp-worker' to install the neccessary modules.\", 'ENOWORKER');\n }\n }));\n sandboxes.push(sandbox);\n }\n\n await Promise.all(sandboxStartPromises);\n\n if (errors.length) {\n console.warn(`Failed to ready ${errors.length} of ${numSandboxes} sandboxes.`, errors);\n throw new Error('Failed to ready sandboxes.');\n }\n\n if (this.readiedSandboxes.length > 0)\n sandboxes.push(...this.readiedSandboxes);\n this.readiedSandboxes = sandboxes;\n\n debugging() && console.log(`readySandboxes: all readied sandboxes ${JSON.stringify(this.readiedSandboxes.map(sandbox => sandbox.id))}`);\n \n return sandboxes;\n }\n\n /**\n * Accepts a sandbox after it has finished working or encounters an error.\n * If the sandbox was terminated or if \"!slice || slice.failed\" then\n * the sandbox will be removed from the sandboxes array and terminated if necessary.\n * Otherwise it will try to distribute a slice to the sandbox immediately.\n *\n * @param {Sandbox} sandbox - the sandbox to return\n * @param {Slice} [slice] - the slice just worked on; !slice => terminate\n */\n returnSandbox (sandbox, slice) {\n if (!slice || slice.failed || sandbox.isTerminated) {\n this.removeElement(this.sandboxes, sandbox);\n if (!sandbox.isTerminated) {\n sandbox.terminate(false);\n }\n }\n\n if (slice) {\n\n const sliceNumber = slice.sliceNumber;\n const jobAddress = slice.jobAddress;\n\n //\n // An upstream failure like certain connection failures, can cause an error cascade,\n // so we need to gaurd against indeterminant state with this conditional.\n // An example of such an error cascade is calling Connection.fetchTransport\n // when this.connectionOptions.transports is empty.\n //\n if (this.authorizationMessages[jobAddress][sliceNumber]) {\n debugging() && console.log(`Supervisor.returnSandbox: ${jobAddress}, ${sliceNumber}, kill corresponding auth message.`);\n /* Done with slice. Cleanup. */\n delete this.authorizationMessages[jobAddress][sliceNumber];\n if (Object.keys(this.authorizationMessages[jobAddress]).length === 0) {\n delete this.authorizationMessages[jobAddress];\n }\n }\n }\n }\n\n /**\n * Terminates sandboxes, in order of creation, when the total started sandboxes exceeds the total allowed sandboxes.\n *\n * @todo Check through the currently started sandboxes and attempt to terminate\n * ones that aren't frequently used.\n * ^^^ This todo may be partially done due to terminating older sandboxes - RR Nov 2019\n * @returns {Promise<void>}\n */\n pruneSandboxes () {\n let numOver = this.sandboxes.length - (dcpConfig.worker.maxAllowedSandboxes + this.maxWorkingSandboxes);\n if (numOver <= 0) return;\n \n // Don't kill readied sandboxes while creating readied sandboxes.\n if (!this.doNotPrune) {\n for (let index = 0; index < this.readiedSandboxes.length; ) {\n const sandbox = this.readiedSandboxes[index];\n // If the sandbox is allocated, advance to the next one in the list.\n if (sandbox.allocated) {\n index++;\n continue;\n }\n // Otherwise, remove this sandbox but look at the same array index in the next loop.\n debugging() && console.log(`pruneSandboxes: Killing readied sandbox ${sandbox.id}`);\n this.readiedSandboxes.splice(index, 1);\n this.returnSandbox(sandbox);\n\n if (--numOver <= 0) break;\n }\n }\n\n if (numOver <= 0) return;\n for (let index = 0; index < this.assignedSandboxes.length; ) {\n const sandbox = this.assignedSandboxes[index];\n // If the sandbox is allocated, advance to the next one in the list.\n if (sandbox.allocated) {\n index++;\n continue;\n }\n // Otherwise, remove this sandbox but look at the same array index in the next loop.\n debugging() && console.log(`pruneSandboxes: Killing assigned sandbox ${sandbox.id}`);\n this.assignedSandboxes.splice(index, 1);\n this.returnSandbox(sandbox);\n\n if (--numOver <= 0) break;\n }\n }\n \n /**\n * Basic watch dog to check if there are idle sandboxes and\n * attempts to nudge the supervisor to feed them work.\n *\n * Run in an interval created in @constructor .\n * @returns {Promise<void>}\n */\n async watchdog () {\n if (!this.watchdogState)\n this.watchdogState = {};\n \n // Every 5 minutes, report progress of all working slices to the scheduler\n if (Date.now() > ((this.lastProgressReport || 0) + sandboxTuning.progressReportInterval)) {\n // console.log('454: Assembling progress update...');\n this.lastProgressReport = Date.now();\n \n const slices = [];\n \n this.queuedSlices.forEach(slice => {\n const jobAddress = slice.jobAddress;\n const sliceNumber = slice.sliceNumber;\n const authorizationMessage = this.checkAuthorization('Supervisor.watchdog:', jobAddress, sliceNumber);\n\n if (authorizationMessage) {\n addToSlicePayload(slices, jobAddress, sliceNumber, 'scheduled', authorizationMessage);\n }\n })\n \n this.workingSandboxes.forEach(sb => {\n // The lifetime of sandbox.isWorking is (or should be) precisely defined,\n // so we can assume sb.slice is always valid here.\n assert(sb.slice);\n\n const jobAddress = sb.jobAddress;\n const sliceNumber = sb.slice.sliceNumber;\n const authorizationMessage = this.checkAuthorization('Supervisor.watchdog:', jobAddress, sliceNumber);\n\n if (authorizationMessage) {\n addToSlicePayload(slices, jobAddress, sliceNumber, 'progress', authorizationMessage);\n }\n });\n\n if (slices.length) {\n // console.log('471: sending progress update...');\n const progressReportPayload = {\n worker: this.workerOpaqueId,\n slices,\n };\n\n this.resultSubmitterConnection.send('status', progressReportPayload)\n .catch(error => {\n console.error('479: Failed to send status update:', error/*.message*/);\n });\n }\n }\n\n if (this.worker.working) {\n if (this.unallocatedSpace > 0) {\n await this.work().catch(err => {\n if (!this.watchdogState[err.code || '0'])\n this.watchdogState[err.code || '0'] = 0;\n if (Date.now() - this.watchdogState[err.code || '0'] > ((dcpConfig.worker.watchdogLogInterval * timeDilation || 120) * 1000))\n console.error('301: Failed to start work:', err);\n this.watchdogState[err.code || '0'] = Date.now();\n });\n }\n\n this.pruneSandboxes();\n }\n }\n\n /**\n * Gets the logical and physical number of cores and also\n * the total number of sandboxes the worker is allowed to run\n *\n */\n getStatisticsCPU() {\n if (DCP_ENV.isBrowserPlatform) {\n return {\n worker: this.workerOpaqueId,\n lCores: window.navigator.hardwareConcurrency,\n pCores: dcpConfig.worker.pCores || window.navigator.hardwareConcurrency,\n sandbox: this.maxWorkingSandboxes\n }\n }\n\n return {\n worker: this.workerOpaqueId,\n lCores: requireNative('os').cpus().length,\n pCores: requireNative('physical-cpu-count'),\n sandbox: this.maxWorkingSandboxes\n }\n }\n\n /**\n * Call to start doing work on the network.\n * This is the one place where requests to fetch new slices are made.\n * After the initial slice and job are fetched it calls @this.distributeQueuedSlices.\n *\n * @returns {Promise<void>}\n */\n async work()\n {\n\n await this.setDefaultIdentityKeystore();\n\n // if connections don't exist, instantiate them. Needed every time worker is started/stopped\n if (!this.taskDistributorConnection)\n this.instantiateAllConnections();\n\n let slicesToFetch;\n if (this.options.priorityOnly && this.options.jobAddresses.length === 0) {\n slicesToFetch = 0;\n } else if (this.queuedSlices.length > 1) {\n // We have slices queued, no need to fetch\n slicesToFetch = 0;\n } else {\n // The queue is almost empty (there may be 0 or 1 element), fetch a full task.\n // The task is full, in the sense that it will contain slices whose\n // aggregate execution time is this.maxWorkingSandboxes * 5-minutes.\n // However, there can only be this.unallocatedSpace # of long slices.\n // Thus we need to know whether a slice in this.queuedSlices is long or not.\n // (A long slice has estimated execution time > 5-minutes.)\n const longSliceCount = (this.queuedSlices.length > 0 && this.queuedSlices[0].isLongSlice) ? 1 : 0;\n slicesToFetch = this.unallocatedSpace - longSliceCount;\n }\n\n debugging() && console.log(`Supervisor.work: Try to get ${slicesToFetch} slices in working sandboxes, unallocatedSpace ${this.unallocatedSpace}, queued slices ${this.queuedSlices.length}`);\n \n // Fetch a new task if we have no more slices queued, then start workers\n try {\n if (slicesToFetch > 0 && !this.isFetchingNewWork) {\n this.isFetchingNewWork = true;\n\n /**\n * This will only ready sandboxes up to a total count of\n * maxWorkingSandboxes (in any state). It is not possible to know the\n * actual number of sandboxes required until we have the slices because we\n * may have sandboxes assigned for the slice's job already.\n */\n if (this.maxWorkingSandboxes > this.sandboxes.length) {\n await this.readySandboxes(\n this.maxWorkingSandboxes - this.sandboxes.length,\n );\n }\n\n /**\n * Temporary change: Assign the capabilities of one of readied sandboxes\n * before fetching slices from the scheduler.\n *\n * TODO: Remove this once fetchTask uses the capabilities of every\n * sandbox to fetch slices.\n */\n if (!this.capabilities) {\n this.capabilities = this.sandboxes[0].capabilities;\n this.emit('capabilitiesCalculated', this.capabilities);\n }\n\n if(DCP_ENV.isBrowserPlatform && this.capabilities.browser)\n this.capabilities.browser.chrome = DCP_ENV.isBrowserChrome;\n\n const fetchTimeout = setTimeout(() => {\n console.warn(`679: Fetch exceeded timeout, will reconnect at next watchdog interval`);\n this.taskDistributorConnection.close();\n this.isFetchingNewWork = false;\n }, 3 * 60 * 1000); // max out at 3 minutes to fetch\n\n // ensure result submitter connection before fetching tasks\n try\n {\n await this.resultSubmitterConnection.keepalive();\n }\n catch (error)\n {\n console.error('Failed to connect to result submitter, refusing to fetch slices. Will try again at next fetch cycle.')\n debugging('supervisor') && console.log(`Error: ${error}`);\n this.isFetchingNewWork = false;\n return;\n }\n await this.fetchTask(slicesToFetch);\n clearTimeout(fetchTimeout);\n }\n \n this.distributeQueuedSlices().then(() => debugging('supervisor') && 'supervisor: finished distributeQueuedSlices()').catch((e)=>console.error(e)) ;\n// await this.distributeQueuedSlices();\n // No catch(), because it will bubble outward to the caller\n } finally {\n this.isFetchingNewWork = false;\n }\n }\n\n /**\n * Generate the workerComputeGroups property of the requestTask message. \n * \n * Concatenate the compute groups object from dcpConfig with the list of compute groups\n * from the supervisor, and remove the public group if accidentally present. Finally,\n * we transform joinSecrets/joinHash into joinHashHash for secure transmission.\n *\n * @note computeGroup objects with joinSecrets are mutated to record their hashes. This\n * affects the supervisor options and dcpConfig. Re-adding a joinSecret property\n * to one of these will cause the hash to be recomputed.\n */\n generateWorkerComputeGroups()\n {\n var computeGroups = Object.values(dcpConfig.worker.computeGroups || {});\n if (this.options.computeGroups)\n computeGroups = computeGroups.concat(this.options.computeGroups);\n computeGroups = computeGroups.filter(group => group.id !== constants.computeGroups.public.id);\n const hashedComputeGroups = [];\n for (const group of computeGroups)\n {\n const groupCopy = Object.assign({}, group);\n if ((group.joinSecret || group.joinHash) && (!group.joinHashHash || this.lastDcpsid !== this.taskDistributorConnection.dcpsid))\n {\n let joinHash;\n if(group.joinHash) {\n joinHash = group.joinHash;\n } else {\n joinHash = calculateJoinHash(groupCopy);\n } \n\n groupCopy.joinHashHash = hash.calculate(hash.eh1, joinHash, this.taskDistributorConnection.dcpsid);\n delete groupCopy.joinSecret;\n delete groupCopy.joinHash;\n debugging('computeGroups') && console.debug(`Calculated joinHash=${joinHash} for`, groupCopy);\n }\n hashedComputeGroups.push(groupCopy);\n }\n this.lastDcpsid = this.taskDistributorConnection.dcpsid;\n debugging('computeGroups') && console.debug('Requesting ', computeGroups.length, 'non-public groups for session', this.lastDcpsid);\n return hashedComputeGroups;\n }\n\n /**\n * Fetches a task, which contains job information and slices for sandboxes and\n * manages events related to fetching tasks so the UI can more clearly display\n * to user what is actually happening.\n * @param {number} numCores\n * @returns {Promise<void>} The requestTask request, resolve on success, rejects otherwise.\n * @emits Supervisor#fetchingTask\n * @emits Supervisor#fetchedTask\n */\n async fetchTask(numCores)\n {\n this.emit('fetchingTask');\n debugging('supervisor') && console.debug('supervisor: fetching task');\n const requestPayload = {\n numCores,\n coreStats: this.getStatisticsCPU(),\n numGPUs: this.defaultMaxGPUs,\n capabilities: this.capabilities,\n paymentAddress: this.paymentAddress,\n jobAddresses: this.options.jobAddresses, // when set, only fetches slices for these jobs\n localExec: this.options.localExec,\n workerComputeGroups: this.generateWorkerComputeGroups(),\n minimumWage: dcpConfig.worker.minimumWage || this.options.minimumWage,\n readyJobs: [ /* list of jobs addresses XXXwg */ ],\n previouslyWorkedJobs: this.cache.jobs,\n };\n\n // workers should be part of the public compute group by default\n if (!booley(dcpConfig.worker.leavePublicGroup) && !booley(this.options.leavePublicGroup))\n requestPayload.workerComputeGroups.push(constants.computeGroups.public);\n debugging('computeGroups') && console.log(`Fetching work for ${requestPayload.workerComputeGroups.length} ComputeGroups: `, requestPayload.workerComputeGroups);\n \n debugging() && console.log(`${shortTime()} fetchTask wants ${numCores} slices, unallocatedSpace ${this.unallocatedSpace}, queuedSlices ${this.queuedSlices.length}`);\n try {\n debugging('requestTask') && debug('requestPayload', requestPayload);\n\n let result = await this.taskDistributorConnection.send('requestTask', requestPayload);\n let responsePayload = result.payload; \n \n if (!result.success) { \n debugging() && console.log('Task fetch failure; request=', requestPayload);\n debugging() && console.log('Task fetch failure; response=', result.payload);\n throw new DCPError('Unable to fetch task for worker', responsePayload);\n }\n\n const sliceCount = responsePayload.body.task.length || 0;\n\n /**\n * The fetchedTask event fires when the supervisor has finished trying to\n * fetch work from the scheduler (task-manager). The data emitted is the\n * number of new slices to work on in the fetched task.\n *\n * @event Supervisor#fetchedTask\n * @type {number}\n */\n this.emit('fetchedTask', sliceCount);\n\n if (sliceCount < 1) {\n return;\n }\n\n /*\n * DCP-1698 Send auth msg with tasks to worker, then validate authority of worker to send slice info back to scheduler.\n * payload structure: { owner: this.address, signature: signature, auth: messageLightWeight, body: messageBody };\n * XXXpfr: @todo Do not think we need 'owner', think about it.\n */\n const { body, ...authorizationMessage } = responsePayload;\n const { newJobs, task } = body;\n assert(newJobs); // It should not be possible to have !newJobs -- we throw on !success.\n for (const jobAddress of Object.keys(newJobs)) {\n this.cache.store('job', jobAddress, newJobs[jobAddress]);\n }\n debugging('supervisor') && console.log(`${shortTime()} fetchTask: Task has ${task.length} slices from ${Object.keys(newJobs).length} new jobs.`);\n\n const tmpQueuedSlices = [];\n for (const taskElement of task) {\n\n if (!this.authorizationMessages[taskElement.jobAddress]) {\n this.authorizationMessages[taskElement.jobAddress] = {};\n // All workers are authorized to send messages to the '0' slice if it is assigned the job.\n this.authorizationMessages[taskElement.jobAddress][0] = authorizationMessage;\n }\n this.authorizationMessages[taskElement.jobAddress][taskElement.sliceNumber] = authorizationMessage;\n\n tmpQueuedSlices.push(new Slice(taskElement));\n debugging('supervisor') && console.log(`/t${shortTime()} fetchTask: slice ${taskElement.sliceNumber}, jobAddress ${taskElement.jobAddress}`);\n }\n\n /** XXXpfr @todo Try to get rid of this.slices. */\n this.slices.push(...tmpQueuedSlices);\n\n this.queuedSlices = [...tmpQueuedSlices, ...this.queuedSlices]; /* Make sure the old ones are up front. */\n\n } catch (error) {\n this.emit('fetchTaskFailed', error);\n debugging('supervisor') && console.debug(`Supervisor.fetchTask failed!: error: ${error}`);\n }\n }\n\n /**\n * For each slice in this.queuedSlices, match with a sandbox in the following order:\n * 1. Try to find an already assigned sandbox in this.assignedSandboxes for the slice's job.\n * 2. Find a ready sandbox in this.readiedSandboxes that is unassigned.\n * 3. Ready a new sandbox and use that.\n *\n * Take great care in assuring sandboxes and slices are uniquely associated, viz.,\n * a given slice cannot be associated with multiple sandboxes and a given sandbox cannot be associated with multiple slices.\n * The lack of such uniqueness has been the root cause of several difficult bugs.\n *\n * @param {number} numSlices The number of slices to match with sandboxes.\n * @returns {Promise<SandboxSlice[]>} Returns SandboxSlice[], may have length zero.\n */\n async matchSlicesWithSandboxes (numSlices) {\n const sandboxSlices = [];\n if (numSlices <= 0 || this.queuedSlices.length == 0 || this.matching)\n return sandboxSlices;\n\n // Don't ask for more than we have.\n if (numSlices > this.queuedSlices.length)\n numSlices = this.queuedSlices.length;\n\n debugging() && console.log(`${shortTime()} matchSlicesWithSandboxes: numSlices ${numSlices}, queued slices ${this.queuedSlices.length}: assigned ${this.assignedSandboxes.length}, readied ${this.readiedSandboxes.length}, total sandbox count: ${this.sandboxes.length}`);\n\n // Auxiliary function to mark sandboxes as allocated so they cannot be pruned out from under us.\n function markAsAllocated(sandboxArray, numMarked) {\n if (numMarked > sandboxArray.length) numMarked = sandboxArray.length;\n for (let k = 1; k <= numMarked; k++) {\n assert(!sandboxArray[sandboxArray.length - k].allocated);\n sandboxArray[sandboxArray.length - k].allocated = true;\n }\n }\n\n try\n {\n this.matching = true;\n const jobAssignedMap = {};\n\n // Until this is rock-solid-stable I want to check for uniquenees.\n if (true) {\n dumpSlicesIfNotUnique(this.queuedSlices, `DANGER: this.queuedSlices slices are not unique -- this is ok when slice is rescheduled.`);\n dumpSandboxesIfNotUnique(this.readiedSandboxes, `DANGER: this.readiedSandboxes sandboxes are not unique!`);\n dumpSandboxesIfNotUnique(this.assignedSandboxes, `DANGER: this.assignedSandboxes sandboxes are not unique!`);\n }\n\n if (debugging()) {\n dumpSlices(this.queuedSlices, 'matchSlicesWithSandboxes(top): this.queuedSlices');\n dumpSandboxes(this.assignedSandboxes, 'matchSlicesWithSandboxes(top): this.assignedSandboxes');\n dumpSandboxes(this.readiedSandboxes, 'matchSlicesWithSandboxes(top): this.readiedSandboxes');\n }\n\n let counter = 0; // General counter.\n let assignedCounter = 0; // How many assigned sandboxes are being used.\n let readyCounter = 0; // How many sandboxes used from the existing this.queuedSlices.\n let newCounter = 0; // How many sandboxes that needed to be newly created.\n\n //\n // The Ideas:\n // 1) We match each slice with a sandbox, in the order that they appear in this.queuedSlices.\n // This allows us to try different orderings of execution of slices. E.g. Wes suggested\n // trying to execute slices from different jobs with maximal job diversity -- specifically\n // if there are 3 jobs j1,j2,j3, with slices s11, s12 from j1, s21, s22, s23 from j2 and\n // s31, s32 from j3, then we try to schedule, in order s11, s21, s31, s12, s22, s32, s23.\n //\n // 2) Before matching slices with sandboxes, we allocate available assigned and readied sandboxes\n // and if more are needed then we create and allocate new ones.\n //\n // 3) Finally we match slices with sandboxes and return an array of sandboxSlice pairs.\n //\n \n //\n // General description of the complexity of promise races.\n // NOTE: Supervisor.returnSandbox does not call any promises and hence does not need to be async, so that has been dropped.\n // The added benefit is that Supervisor.pruneSandboxes is also not async anymore and hence there is better safety with the\n // race between Supervisor.pruneSandboxes and Supervisor.readySandboxes. Specifically, during pruneSandboxes there are no\n // promise calls which could result in 'async readySandboxes' (which creates new 'readied' sandboxes) being called in the midst\n // of pruning 'readied' sandboxes. There is still a race in 'async matchSlicesWithSandboxes' when calling 'async readySandboxes'\n // where it is possible to interleave with 'async watchdog' which calls pruneSandboxes, but this race is prevented by setting\n // 'this.doNotPrune = true;' in 'async matchSlicesWithSandboxes' right before the call to 'async readySandboxes', because\n // 'if (!this.doNotPrune)' guards pruning 'readied' sandboxes.\n //\n\n // Associate assigned sandboxes for a given jobAddress with the corresponding count of slices owned\n // by the same jobAddress.\n for (let index = this.queuedSlices.length - 1; index >= 0; index--) {\n const slice = this.queuedSlices[index];\n assert(slice.isUnassigned);\n\n const jobAddress = slice.jobAddress.valueOf();\n let assignedState = jobAssignedMap[jobAddress];\n\n if (!assignedState) {\n // Get all assigned sandboxes for jobAddress.\n assignedState = { sandboxes: this.assignedSandboxes.filter(sb => sb.jobAddress.valueOf() === jobAddress), count: 1 };\n jobAssignedMap[jobAddress] = assignedState;\n } else {\n assignedState.count++;\n }\n\n if (++counter >= numSlices)\n break;\n }\n counter = 0;\n\n // Calculate how many new sandboxes are needed.\n for (const [jobAddress, assignedState] of Object.entries(jobAssignedMap)) {\n // Count the # of assigned sandboxes we can use.\n const assignedUsedCount = Math.min(assignedState.count, assignedState.sandboxes.length);\n assignedCounter += assignedUsedCount;\n\n // Mark the assigned sandboxes we can use as allocated, so they're not pruned out from under us.\n markAsAllocated(assignedState.sandboxes, assignedUsedCount);\n\n // Count slices w/o assigned sandbox.\n const count = Math.max(assignedState.count - assignedState.sandboxes.length, 0);\n counter += count;\n debugging() && console.log(`matchSlicesWithSandboxes: job ${jobAddress}, assigned ${assignedState.sandboxes.length}, slices ${assignedState.count}, need ${count}/${this.readiedSandboxes.length} readied sandboxes.`);\n }\n\n counter -= this.readiedSandboxes.length;\n if (counter > 0) { // Number of new sandboxes needed.\n newCounter = counter;\n readyCounter = this.readiedSandboxes.length;\n // Don't prune readied sandboxes while creating readied sandboxes -- yes, it happens...\n this.doNotPrune = true;\n try { await this.readySandboxes(newCounter); } finally { this.doNotPrune = false; }\n } else {\n readyCounter = this.readiedSandboxes.length + counter;\n }\n counter = 0;\n\n debugging() && console.log(`matchSlicesWithSandboxes: newCounter ${newCounter}, readyCounter ${readyCounter}, assignedCounter ${assignedCounter}, total readied ${this.readiedSandboxes.length}`);\n\n // Validate algorithm consistency.\n if (Supervisor.debugBuild && assignedCounter + readyCounter + newCounter !== numSlices) {\n // Structured assert.\n throw new DCPError(`matchSlicesWithSandboxes: Algorithm is corrupt ${assignedCounter} + ${readyCounter} + ${newCounter} !== ${numSlices}`);\n }\n\n // Mark the readied sandboxes we can use as allocated, so they're not pruned out from under us.\n // Note: We could run markAsAllocated before the async call to readySandboxes, which would gaurd from pruning,\n // then run it again on the brand new sandboxes just created, but it makes the code hard to read and isn't necessary,\n // beacause the doNotPrune trick seems to work. We also check if we unexpectedly run out of sandboxes (assert in debug.)\n markAsAllocated(this.readiedSandboxes, readyCounter + newCounter);\n\n while (this.queuedSlices.length > 0) {\n const slice = this.queuedSlices.pop();\n assert(slice.isUnassigned);\n\n const jobAddress = slice.jobAddress.valueOf();\n const assignedSandboxesForJob = jobAssignedMap[jobAddress].sandboxes;\n assert(assignedSandboxesForJob);\n\n if (assignedSandboxesForJob.length > 0) {\n const sandbox = assignedSandboxesForJob.pop();\n this.removeElement(this.assignedSandboxes, sandbox);\n assert(jobAddress === sandbox.jobAddress.valueOf());\n\n debugging() && console.log(`matchSlicesWithSandboxes: Assigned sandbox matched ${this.dumpSandboxAndSlice(sandbox, slice)}`);\n sandboxSlices.push({ sandbox, slice });\n } else {\n // There may be a race between creating and pruning, though it should be fixed with the doNotPrune trick.\n assert(this.readiedSandboxes.length > 0);\n if (this.readiedSandboxes.length > 0) {\n const sandbox = this.readiedSandboxes.pop();\n debugging() && console.log(`matchSlicesWithSandboxes: Readied sandbox matched ${this.dumpSandboxAndSlice(sandbox, slice)}`);\n sandboxSlices.push({ sandbox, slice });\n }\n }\n\n if (++counter >= numSlices)\n break;\n }\n\n debugging() && console.log(`matchSlicesWithSandboxes: Matches: ${ JSON.stringify(sandboxSlices.map(ss => this.dumpSandboxSlice(ss))) }`);\n\n // Until this is rock-solid-stable I want to check for uniquenees.\n if (true) {\n this.dumpSandboxSlicesIfNotUnique(sandboxSlices, `DANGER: sandboxSlices; { sandbox, slice } pairs are not unique!`);\n }\n\n if (debugging()) {\n console.log(`matchSlicesWithSandboxes: found ${sandboxSlices.length} sandboxes for jobs ${JSON.stringify(Object.keys(jobAssignedMap))}: assigned ${assignedCounter}, ready ${readyCounter}, new ${newCounter}`);\n this.dumpSandboxSlices(sandboxSlices, 'matchSlicesWithSandboxes: sandboxSlices');\n dumpSandboxes(this.assignedSandboxes, 'matchSlicesWithSandboxes: this.assignedSandboxes');\n dumpSandboxes(this.readiedSandboxes, 'matchSlicesWithSandboxes: this.readiedSandboxes');\n console.log(`matchSlicesWithSandboxes: remaining readied: ${JSON.stringify(this.readiedSandboxes.map(s => s.id))}`);\n }\n\n } catch (e) {\n console.error(`DANGER matchSlicesWithSandboxes threw exception`, e);\n //throw e; // Should we rethrow?\n } finally {\n this.matching = false;\n }\n\n debugging() && console.log(`${shortTime()} matchSlicesWithSandboxes allocated ${sandboxSlices.length} sandboxes, queuedSlices ${this.queuedSlices.length}, unallocatedSpace ${this.unallocatedSpace}.`);\n\n return sandboxSlices;\n }\n\n /**\n * This method will call @this.startSandboxWork(sandbox, slice) for { sandbox, slice }\n * in the array returned by @this.matchSlicesWithSandboxes(availableSandboxes) until all sandboxes are working.\n * It is possible for a sandbox to finish simultaneously and leave a sandbox that is not working.\n * Note: @this.queuedSlices may be exhausted before all sandboxes are working.\n * @returns {Promise<void>}\n */\n async distributeQueuedSlices () {\n //\n // All matching of sandbox with slice is taken care of by @this.matchSlicesWithSandboxes().\n // We do not use @this.snapshotOfQueuedSlices becuase it is in a fuzzy state; when matched with a sandbox,\n // a slice transitions to working at an unknown point in the future.\n // Instead we use @this.queuedSlices which really is a queue, whose elements are\n // dequeued in @this.matchSlicesWithSandboxes() and enqueued in fetchTask.\n // We should try to get rid of @this.slices, because it has low utility.\n // However, to minimize churn, this can be done later.\n //\n\n const availableSandboxes = this.unallocatedSpace;\n if (availableSandboxes <= 0) return;\n\n debugging() && console.log(`distributeQueuedSlices: unallocatedSpace ${availableSandboxes}, readied ${JSON.stringify(this.readiedSandboxes.map(s => s.id))}, queuedSlices ${this.queuedSlices.length}`);\n\n const sandboxSlices = await this.matchSlicesWithSandboxes(availableSandboxes);\n\n for (let sandboxSlice of sandboxSlices) {\n\n const { sandbox, slice } = sandboxSlice;\n if (sandbox) {\n debugging() && console.log(`${shortTime()}: distributeQueuedSlices: matched slice ${slice.sliceNumber} with sandbox ${sandbox.id} for job ${slice.jobAddress}, total sandbox count: ${this.sandboxes.length}`);\n if (sandbox.isReadyForAssign) {\n try {\n let timeoutMs = Math.floor(Math.min(+Supervisor.lastAssignFailTimerMs || 0, 10 * 60 * 1000 /* 10m */));\n await asleepMs(timeoutMs);\n if (sandbox.isReadyForAssign) { // Don't need this double-check anymore because of @this.matchSlicesWithSandboxes().\n sandbox.setIsAssigning(); // Don't need this either --> Set the state to assigning before the await to circumvent the event loop problem from await.\n await this.assignJobToSandbox(sandbox, slice.jobAddress);\n } else {\n return // Don't need this either --> The sandbox was picked a second time while it was being prepared, return without an error\n }\n } catch (e) {\n console.error(`Could not assign slice ${slice.sliceNumber} for job ${slice.jobAddress} to sandbox ${sandbox.id}`);\n if (Supervisor.debugBuild) console.error(`...exception`, e);\n Supervisor.lastAssignFailTimerMs = Supervisor.lastAssignFailTimerMs ? +Supervisor.lastAssignFailTimerMs * 1.25 : Math.random() * 200;\n this.returnSlice(slice);\n continue;\n }\n }\n\n if (!Supervisor.lastAssignFailTimerMs)\n Supervisor.lastAssignFailTimerMs = Math.random() * 200;\n this.startSandboxWork(sandbox, slice);\n Supervisor.lastAssignFailTimerMs = false;\n\n } else {\n // We should never get here.\n console.error(\"Supervisor.distributeQueuedSlices: Failed to find sandbox for slice\", {\n jobAddress: slice.jobAddress,\n sliceNumber: slice.sliceNumber\n });\n }\n }\n }\n\n /**\n *\n * @param {Sandbox} sandbox\n * @param {opaqueId} jobAddress\n * @returns {Promise<void>}\n */\n async assignJobToSandbox(sandbox, jobAddress) {\n var ceci = this;\n\n try {\n return sandbox.assign(jobAddress); // Returns Promise.\n } catch(error) {\n // return slice to scheduler, log error\n console.error('Supervisor.assignJobToSandbox: Failed to assign job to sandbox.', {\n jobAddress: jobAddress.substr(0,10),\n error,\n });\n\n ceci.returnSandbox(sandbox);\n\n throw error;\n }\n }\n \n /**\n * Gives a slice to a sandbox which begins working. Handles collecting\n * the slice result (complete/fail) from the sandbox and submitting the result to the scheduler.\n * It will also return the sandbox to @this.returnSandbox when completed so the sandbox can be re-assigned.\n *\n * @param {Sandbox} sandbox - the sandbox to give the slice\n * @param {Slice} slice - the slice to distribute\n * @returns {Promise<void>} Promise returned from sandbox.run\n */\n async startSandboxWork (sandbox, slice) {\n var startDelayMs;\n\n try {\n slice.markAsWorking();\n } catch (e) {\n // This will occur when the same slice is distributed twice.\n // It is normal because two sandboxes could finish at the same time and be assigned the\n // same slice before the slice is marked as working.\n debugging() && console.debug(e);\n return;\n }\n\n // sandbox.requiresGPU = slice.requiresGPU;\n // if (sandbox.requiresGPU) {\n // this.GPUsAssigned++;\n // }\n\n if (Supervisor.startSandboxWork_beenCalled)\n startDelayMs = 1000 * (tuning.minSandboxStartDelay + (Math.random() * (tuning.maxSandboxStartDelay - tuning.minSandboxStartDelay)));\n else {\n startDelayMs = 1000 * tuning.minSandboxStartDelay;\n Supervisor.startSandboxWork_beenCalled = true;\n }\n \n try {\n debugging() && console.log(`${shortTime()} startSandboxWork: started sandbox.id.sliceNumber ${sandbox.id}-${slice.sliceNumber}.${slice.jobAddress}, total sandbox count: ${this.sandboxes.length}`);\n let result = await sandbox.work(slice, startDelayMs);\n slice.collectResult(result, true);\n // In watchdog, all sandboxes in 'working' state, have their status sent to result submitter.\n // However, this can happen after the sandbox/slice has already sent results\n // to result submitter, in which case, the activeSlices table has already removed the row\n // corresponding to slice and hence is incapable of updating status.\n sandbox.changeWorkingToAssigned();\n sandbox.allocated = false; // This is a little redundant: don't await any promises between here and the finally clause.\n this.assignedSandboxes.push(sandbox);\n debugging() && console.log(`${shortTime()} startSandboxWork: completed sandbox.id.sliceNumber ${sandbox.id}-${slice.sliceNumber}.${slice.jobAddress}, total sandbox count: ${this.sandboxes.length}`);\n } catch(error) {\n let logLevel;\n if (error instanceof SandboxError) {\n logLevel = 'warn';\n // The message and stack properties of error objects are not enumerable,\n // so they have to be copied into a plain object this way\n const errorResult = Object.getOwnPropertyNames(error).reduce((o, p) => {\n o[p] = error[p]; return o;\n }, { message: 'Unexpected worker error' });\n slice.collectResult(errorResult, false);\n } else {\n logLevel = 'error';\n // This error was unrelated to the work being done,\n // so just return the slice in the finally block.\n }\n\n // sandbox.public.name is defined in Sandbox.assign.\n // It is possible to get here with !sandbox.public.\n const jobName = sandbox.public ? sandbox.public.name : 'unnamed';\n const errorObject = {\n jobAddress: slice.jobAddress.substr(0,10),\n sliceNumber: slice.sliceNumber,\n sandbox: sandbox.id,\n jobName: jobName,\n };\n\n // XXXpfr Enabled Informative sandbox errors when in debug mode.\n if (!Supervisor.debugBuild && error.errorCode === 'EUNCAUGHTERROR') {\n console[logLevel](`Supervisor.startSandboxWork: Uncaught error in sandbox, could not compute.`, errorObject);\n } else {\n if (Supervisor.debugBuild) // Don't show stack traces in release builds.\n errorObject.stack = error.stack;\n console[logLevel](`Supervisor.startSandboxWork: Sandbox failed (${error.message})`, errorObject);\n }\n } finally {\n sandbox.allocated = false;\n if (slice.result) {\n await this.recordResult(slice);\n } else if (slice.error) {\n this.returnSlice(slice);\n } else {\n this.returnSlice(slice);\n }\n this.returnSandbox(sandbox, slice);\n }\n }\n\n /**\n * Terminates sandboxes and returns slices.\n * Sets the working flag to false, call @this.work to start working again.\n * \n * If forceTerminate is true: Terminates all sandboxes and returns all slices.\n * If forceTerminate is false: Terminates non-working sandboxes and returns queued slices.\n *\n * @param {boolean} [forceTerminate = true] - true if you want to stop the sandboxes from completing their current slice.\n * @returns {Promise<void>}\n */\n async stopWork (forceTerminate = true) {\n if (forceTerminate) {\n let sandbox;\n while (sandbox = this.sandboxes.pop()) {\n sandbox.terminate(false);\n }\n\n await this.returnSlices(this.slices).then(() => {\n this.slices.length = 0;\n this.queuedSlices.length = 0;\n });\n\n this.resultSubmitterConnection.off('close', this.openResultSubmitterConn);\n this.resultSubmitterConnection.close();\n this.resultSubmitterConnection = null;\n } else {\n // Only terminate idle sandboxes and return only queued slices\n let idleSandboxes = this.sandboxes.filter(w => !w.isWorking);\n for (let sandbox of idleSandboxes) {\n sandbox.terminate(false);\n }\n\n let queuedSlices = this.queuedSlices;\n await this.returnSlices(queuedSlices).then(() => {\n // Kill corresponding entries in this.slices .\n this.queuedSlices.forEach(slice => {\n this.removeElement(this.slices, slice);\n });\n this.queuedSlices.length = 0;\n });\n await new Promise((resolve, reject) => {\n let sandboxesRemaining = this.workingSandboxes.length;\n if (sandboxesRemaining === 0)\n {\n resolve();\n }\n // Resolve and finish work once all sandboxes have finished submitting their results.\n this.on('submitFinished', () => {\n sandboxesRemaining--;\n if (sandboxesRemaining === 0)\n {\n console.log('All sandboxes empty, stopping worker and closing all connections');\n resolve();\n }\n });\n })\n this.resultSubmitterConnection.off('close', this.openResultSubmitterConn);\n this.resultSubmitterConnection.close();\n this.resultSubmitterConnection = null;\n }\n\n this.taskDistributorConnection.off('close', this.openTaskDistributorConn);\n this.taskDistributorConnection.close();\n this.taskDistributorConnection = null;\n\n this.packageManagerConnection.off('close', this.openPackageManagerConn);\n this.packageManagerConnection.close();\n this.packageManagerConnection = null;\n\n this.eventRouterConnection.off('close', this.openEventRouterConn);\n this.eventRouterConnection.close();\n this.eventRouterConnection = null;\n\n this.emit('stop');\n }\n\n /**\n * Takes a slice and returns it to the scheduler to be redistributed.\n * Usually called when the supervisor told it to forcibly stop working.\n *\n * @param {Slice} slice - the slice to return to the scheduler\n * @returns {Promise<*>} - response from the scheduler the slice was returned to\n */\n returnSlice (slice) {\n // Remove the slice from the slices array.\n this.removeElement(this.slices, slice);\n\n const jobAddress = slice.jobAddress;\n const sliceNumber = slice.sliceNumber;\n const authorizationMessage = this.checkAuthorization('Supervisor.returnSlice:', jobAddress, sliceNumber);\n if (!authorizationMessage) return undefined;\n\n let payload = {};\n if (slice.error) {\n payload = slice.getReturnMessagePayload(this.workerOpaqueId, authorizationMessage, 'uncaught');\n } else {\n payload = slice.getReturnMessagePayload(this.workerOpaqueId, authorizationMessage);\n }\n\n return this.resultSubmitterConnection.send('status', payload)\n .then(response => {\n return response;\n })\n .catch(error => {\n console.error('Failed to return slice', {\n sliceIdentifier: slice.identifier,\n sliceNumber: sliceNumber,\n jobAddress: jobAddress,\n error,\n });\n })\n }\n\n /** Bulk-return multiple slices, possibly for assorted jobs\n */\n returnSlices(slices) {\n const slicePayload = [];\n\n slices.forEach(slice => {\n addToSlicePayload(slicePayload, slice.jobAddress, slice.sliceNumber, 'return', this.checkAuthorization('Supervisor.returnSlices:', slice.jobAddress, slice.sliceNumber));\n });\n\n return this.resultSubmitterConnection.send('status', {\n worker: this.workerOpaqueId,\n slices: slicePayload,\n });\n }\n\n /**\n * Submits the slice results to the scheduler, either to the\n * work submit or fail endpoints based on the slice status.\n * Then remove the slice from the @this.slices cache.\n *\n * @param {Slice} slice - the slice to submit\n * @returns {Promise<void>}\n */\n async recordResult (slice) {\n const metrics = { GPUTime: 0, CPUTime: 0, CPUDensity: 0, GPUDensity: 0 }; \n \n debugging('supervisor') && console.log('supervisor: recording result');\n assert(slice && slice.hasOwnProperty('result'), slice.sliceNumber, slice.jobAddress);\n\n const jobAddress = slice.jobAddress;\n const sliceNumber = slice.sliceNumber;\n const authorizationMessage = this.checkAuthorization('Supervisor.recordResult:', jobAddress, sliceNumber);\n if (!authorizationMessage) return; // Question: Is it better to proceed and have a cascade of errors?\n\n /* @see result-submitter::result for full message details */\n var payloadData = {\n slice: sliceNumber,\n job: jobAddress,\n worker: this.workerOpaqueId,\n paymentAddress: this.paymentAddress,\n metrics,\n authorizationMessage,\n }\n const isValidTimeReport = slice.result && slice.result.timeReport && slice.result.timeReport.total > 0;\n if (isValidTimeReport) {\n const timeReport = slice.result.timeReport;\n assert(timeReport.total);\n metrics.GPUTime = timeReport.webGL;\n metrics.CPUTime = timeReport.total - (metrics.GPUTime + timeReport.idle);\n metrics.CPUDensity = metrics.CPUTime / timeReport.total;\n metrics.GPUDensity = metrics.GPUTime / timeReport.total;\n }\n \n this.emit('submittingResult');\n\n if (!slice.isFinished)\n throw new Error('Cannot record result for slice that is not finished');\n\n if (slice.resultStorageType === 'pattern') /* this is a remote-storage slice */\n payloadData.result = await sendResultToRemote(slice);\n else {\n // It is possible for slice.result to be undefined when there are upstream errors.\n if (!slice.result)\n throw new Error('slice.result is undefined. This is ok when there are upstream errors.');\n payloadData.result = encodeDataURI(slice.result.result); /* XXXwg - result.result is awful */\n }\n\n try {\n if (slice.completed) {\n\n /* work function returned a result */\n const { success, payload } = await this.resultSubmitterConnection.send(\n 'result',\n payloadData,\n );\n\n if (!success) {\n throw new DCPError('Unable to submit result for work done', payload);\n }\n\n const receipt = {\n accepted: true,\n payment: payload.slicePaymentAmount,\n };\n this.emit('submittedResult', payload);\n this.emit('dccCredit', receipt);\n } else {\n\n /* slice did not complete for some reason */\n let statusPayloadData = {\n worker: this.workerOpaqueId,\n slices: [\n {\n sliceNumber: sliceNumber,\n job: jobAddress,\n status: 'return', // special state looked for in status.js / result-submitter\n reason: 'uncaught', // special state looked for in status.js / result-submitter\n /** @todo XXXpfr: Is there error info we can use here? */ \n // error: slice.error, \n authorizationMessage,\n }\n ], \n };\n \n await this.resultSubmitterConnection.send('status', statusPayloadData);\n }\n } catch(error) {\n console.info(`1014: Failed to submit results for slice ${payloadData.slice} of job ${payloadData.job}`, error);\n this.emit('submitSliceFailed', error);\n }\n finally\n {\n this.emit('submitFinished');\n // Remove the slice from the slices array\n this.removeElement(this.slices, slice);\n }\n }\n}\n\n/** \n * Send a work function's result to a server that speaks our DCP Storage Server protocol.\n * A sample storage server has been implemented in https://gitlab.com/Distributed-Compute-Protocol/dcp-storage-server\n * as part of DCP-1479.\n *\n * @param {Slice} slice slice object whose result we are sending\n * @returns {Promise<object>} object with properties contentType and uri\n * @throws if the remote server returned a non-ok status object, or an HTTP status not in the 2xx range.\n */\nasync function sendResultToRemote(slice) {\n const postParams = {\n ...slice.resultStorageParams\n };\n\n const sliceResultUri = makeSliceURI('pattern', slice.resultStorageDetails, {\n slice: slice.sliceNumber,\n job: slice.jobAddress,\n //juuid: slice.job.uuid -- what should be here?\n });\n\n const url = new DcpURL(sliceResultUri);\n\n if (dcpConfig.worker.allowOrigins.any.indexOf(url.origin) === -1 &&\n dcpConfig.worker.allowOriginResultStorage.indexOf(url.origin) === -1) {\n throw new Error(`Invalid origin for remote result storage: '${url.origin}'`);\n }\n \n postParams.slice = slice.sliceNumber;\n postParams.job = slice.jobAddress;\n postParams.contentType = 'application/json'; // Currently data will be outputed as a JSON object, @todo: Support file upload\n\n if (slice.result.result)\n postParams.result = slice.result.result;\n else\n postParams.error = new Error('no result'); /** @todo XXXwg return the error object from the sandbox */\n\n const formBodyArray = [];\n for (const property in postParams) {\n const encodedKey = encodeURIComponent(property);\n const encodedValue = encodeURIComponent(postParams[property]);\n formBodyArray.push(`${encodedKey}=${encodedValue}`);\n }\n const formBody = formBodyArray.join('&');\n\n let postPromise = new Promise(function supervisorPostResult(resolve, reject) {\n let deeperErrorStack = new Error().stack;\n deeperErrorStack = deeperErrorStack.substring(deeperErrorStack.indexOf('\\n') + 1);\n const xhr = new XMLHttpRequest();\n xhr.onloadend = function supervisor$$recordResult$onloadend() {\n try {\n let o;\n \n delete xhr.onloadend;\n if (xhr.status >= 200 && xhr.status < 300) {\n /* Successful post: record the resultant URL; needed for dcp-client application to be able to fetch the result */\n switch(xhr.getResponseHeader('content-type'))\n {\n default: /* support lousy web servers */\n case 'application/json': {\n o = JSON.parse(xhr.responseText);\n break;\n }\n case 'application/x-kvin': {\n o = __webpack_require__(/*! kvin */ \"./node_modules/kvin/kvin.js\").deserialize(xhr.responseText);\n break;\n }\n }\n\n if (o.status === 'ok')\n return resolve(o);\n \n return reject(new Error(`Remote server ${url} did not set status=ok (o.error ? ${JSON.stringify(o)} : error)`));\n } else {\n const error = new Error(`HTTP Error ${xhr.status} sending slice results to ${url}`);\n //\n // Note: This impl is mostly grafted from justFetch in dcp/src/protocol-v3/index.\n // It is a safe assumption that this function has never been tested.\n // Refine the following error info when we support remote-storage.\n //\n error.request = xhr;\n error.request.location = url;\n error.request.method = 'POST';\n error.request.status = xhr.status;\n error.stack += '\\n----------\\n' + deeperErrorStack;\n throw error;\n }\n } catch (e) {\n return reject(e);\n }\n\n return reject(new Error('impossible'));\n }\n \n xhr.onreadystatechange = function() {\n if (this.readyState == 4 && this.status == 200) {\n debugging('supervisor') && console.log('Response text', this.responseText);\n }\n }\n \n xhr.open('POST', url.href, true);\n xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');\n xhr.send(formBody);\n });\n\n return postPromise;\n}\n\n/**\n * Return DCPv4-specific connection options, composed of type-specific, URL-specific, \n * and worker-specific options, any/all of which can override the dcpConfig.dcp.connectOptions.\n * The order of precedence is the order of specificity.\n */\nfunction connectionOptions(url, label) {\n return leafMerge(/* ordered from most to least specific */\n dcpConfig.worker.dcp.connectionOptions.default,\n dcpConfig.worker.dcp.connectionOptions[label],\n dcpConfig.worker.dcp.connectionOptions[url.href]);\n}\n\n\n/**\n * Add a slice to the slice payload being built. If a sliceList already exists for the \n * job-status-authMessage tuple, then the slice will be added to that, otherwise a new \n * sliceList will be added to the payload.\n *\n * @param {Object[]} slicePayload Slice payload being built. Will be mutated in place.\n * @param {Address} job Address of job the slice belongs to\n * @param {Number} sliceNumber Slice number\n * @param {String} status Status update, eg. progress or scheduled\n * @param {Object} authorizationMessage authorizationMessage for the slice\n *\n * @returns {Object[]} mutated slicePayload array\n */\nfunction addToSlicePayload(slicePayload, job, sliceNumber, status, authorizationMessage) {\n // Try to find a sliceList in the payload which matches the job, status, and auth message\n let sliceList = slicePayload.find(desc => {\n return desc.job === job && desc.status === status && desc.authorizationMessage === authorizationMessage;\n });\n\n // If we didn't find a sliceList, start a new one and add it to the payload\n if (!sliceList) {\n sliceList = {\n job,\n sliceNumbers: [],\n status,\n authorizationMessage: authorizationMessage,\n };\n slicePayload.push(sliceList);\n }\n\n sliceList.sliceNumbers.push(sliceNumber);\n\n return slicePayload;\n}\n\n/** @type {number | boolean} */\nSupervisor.lastAssignFailTimerMs = false;\n/** @type {boolean} */\nSupervisor.startSandboxWork_beenCalled = false;\n/** @type {boolean} */\nSupervisor.debugBuild = (__webpack_require__(/*! dcp/common/dcp-build */ \"./src/common/dcp-build.js\").build === 'debug');\n\nexports.Supervisor = Supervisor;\n\n\n//# sourceURL=webpack:///./src/dcp-client/worker/supervisor.js?");
5407
5407
 
5408
5408
  /***/ }),
5409
5409
 
@@ -5426,7 +5426,7 @@ eval("/* WEBPACK VAR INJECTION */(function(process) {/**\n * @file debugg
5426
5426
  /*! no static exports found */
5427
5427
  /***/ (function(module, exports, __webpack_require__) {
5428
5428
 
5429
- eval("/**\n * @file events/event-subscriber.js\n * @author Ryan Rossiter <ryan@kingsds.network>\n * @date March 2020\n * \n * This file is the client-side companion to the event-router.\n * It maintains a map of subscription tokens that the event router has provisioned\n * for it, and calls the associated callbacks when the event router emits a new event.\n */\n\nconst protocolV4 = __webpack_require__(/*! dcp/protocol-v4 */ \"./src/protocol-v4/index.js\");\nconst { DcpURL } = __webpack_require__(/*! dcp/common/dcp-url */ \"./src/common/dcp-url.js\");\nconst debugging = __webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope('event-subscriber');\n\n/** @typedef {import('./common-types').DcpEvent} DcpEvent */\n/** @typedef {import('./common-types').SubscriptionToken} SubscriptionToken */\n/** @typedef {import('./common-types').EventLabel} EventLabel */\n/** @typedef {import('./common-types').SubscriptionOptions} SubscriptionOptions */\n\n/**\n * A unique identifier for the client of a subscription.\n * @typedef {number} SubscriptionId\n */\n\n/**\n * A container for callback and SubscriptionToken pairs\n * @typedef {Object} CallbackHandle\n * @property {SubscriptionToken} token\n * @property {function} callback\n */\n\n/**\n * @typedef {Object} Subscription\n * @property {EventLabel} label - Event label\n * @property {function[]} callbacks - The callbacks that will be invoked when an event is received for this subscription\n * @property {SubscriptionOptions} options - Additional options\n * @property {string} optionsStr - A string used to memoize stringifying the options object\n */\n\nlet idCounter = 1; // Start at 1 so it's truthy :)\n\nclass EventSubscriber {\n constructor() {\n /** @type {Map<SubscriptionToken, Subscription>} */\n this.subscriptions = new Map();\n\n // Since tokens will be refreshed on reconnect, we have to\n // give the callers of subscribe a token that will not change on reconnect.\n /** @type {Map<SubscriptionId, CallbackHandle>} */\n this.subscriptionIds = new Map();\n\n /** @type {Interval} Interval to keep the connection alive when no messages\n * are being received */\n this._keepaliveInterval = null;\n\n this.onEventRouterConnectionInterrupted = () => {\n this.openEventRouterConn();\n this.setupEventRouterConnectionEvents();\n this.reestablishSubscriptions();\n }\n\n const ceci = this;\n\n this.eventRouterConnection = null;\n this.openEventRouterConn = function openBankConn()\n {\n ceci.eventRouterConnection = new protocolV4.Connection(dcpConfig.scheduler.services.eventRouter);\n ceci.eventRouterConnection.on('close', ceci.onEventRouterConnectionInterrupted);\n }\n this.openEventRouterConn();\n this.setupEventRouterConnectionEvents();\n }\n\n async close() {\n debugging() && console.log(`event-subscriber: closing EventSubscriber connection`);\n this.eventRouterConnection.off('close', this.onEventRouterConnectionInterrupted);\n this.eventRouterConnection.on('close', this.openEventRouterConn);\n await this.eventRouterConnection.close()\n .finally(() => {\n this.subscriptions.clear();\n this.subscriptionIds.clear();\n this.hookedEventRouterConnectionEvents = false;\n });\n }\n\n async reestablishSubscriptions() {\n let subs = this.subscriptions.entries();\n debugging() && console.log(`event-subscriber: reestablishing ${this.subscriptions.size} subscriptions`);\n\n const oldSubs = new Map(this.subscriptions);\n this.subscriptions = new Map();\n for (let [oldToken, sub] of subs) {\n try {\n let newToken = await this.provisionSubscriptionToken(sub.label, sub.options);\n this.subscriptions.set(newToken, sub);\n oldSubs.delete(oldToken);\n this.releaseSubscriptionToken(sub.label, oldToken).catch(error => {\n debugging('verbose') && console.log(`event-subscriber: failed to release old token:`, error.message);\n });\n\n // Now update the token in this.subscriptionIds\n for (let cbHandle of this.subscriptionIds.values()) {\n if (cbHandle.token === oldToken) {\n cbHandle.token = newToken;\n }\n }\n }\n catch (error) {\n debugging() && console.warn(`event-subscriber: error establishing subscription (${sub.label}):`, error);\n }\n }\n\n if (oldSubs.size) {\n debugging() && console.warn(`event-subscriber: failed to reestablish ${oldSubs.size} old subscriptions:`);\n\n for (let [token, sub] of oldSubs.entries()) {\n debugging() && console.warn(` ${sub.label}`);\n this.subscriptions.set(token, sub);\n }\n }\n }\n\n setupEventRouterConnectionEvents() {\n debugging() && console.log(`event-subscriber: setupEventRouterConnectionEvents`)\n this.eventRouterConnection.on('request', (req) => {\n const { operation } = req.payload;\n if (operation === 'event') this.onEventRequest(req);\n else req.respond(new Error(`Unknown EventSubscriber operation: ${operation}`));\n });\n }\n\n async onEventRequest(req) {\n debugging('verbose') && console.log(`event-subscriber: onEventRequest`);\n const { token, event } = req.payload.data;\n if (this.subscriptions.has(token)) {\n const { label, callbacks } = this.subscriptions.get(token);\n debugging('verbose') && console.log(`event-subscriber: event: ${label}`);\n if (label !== event.label) {\n console.warn(`Rejecting event with mismatched label: ${event.label} should be ${label}`);\n req.respond(new Error(`Event label ${event.label} doesn't match expected ${label}`));\n } else if (!callbacks.length) {\n req.respond(new Error(`Token is registered but no callbacks setup for event label ${label}`));\n } else {\n // wait for all callbacks to be invoked before acknowledging the request\n await Promise.all(callbacks.map(async (cb) => {\n try {\n await cb(event.data);\n } catch (e) {\n console.error(`Error thrown by callback for event '${label}':`);\n console.error(e);\n }\n }));\n\n req.respond();\n }\n } else {\n req.respond(new Error(`No subscription registered for token ${token}`));\n }\n }\n\n /**\n * Registers this subscription with the event router, and saves the returned\n * subscription token into the this.subscriptions map.\n * @param {EventLabel} label\n * @param {SubscriptionOptions} options\n * @returns {Promise<SubscriptionId>}\n */\n async provisionSubscriptionToken(label, options) {\n debugging('verbose') && console.log(`event-subscriber: provisionSubscriptionToken(${label})`);\n const res = await this.eventRouterConnection.send('subscribe', {\n label, options,\n });\n \n const token = res.payload.token;\n this.subscriptions.set(token, {\n label, callbacks: [], options,\n // optionsStr is caching the stringified options,\n // for comparing against future subscriptions\n optionsStr: JSON.stringify(options),\n });\n\n return token;\n }\n\n /**\n * Unregisters a subscription from the event router\n * @param {EventLabel} label\n * @param {SubscriptionToken} token \n */\n async releaseSubscriptionToken(label, token) {\n debugging('verbose') && console.log(`event-subscriber: releaseSubscriptionToken(${label})`);\n await this.eventRouterConnection.send('unsubscribe', {\n label, token,\n });\n }\n\n /**\n * @param {EventLabel} label - Event label\n * @param {function} callback\n * @param {SubscriptionOptions} options - Additional options\n * @returns {Promise<SubscriptionId>}\n */\n async subscribe(label, callback, options={}) {\n debugging() && console.log(`event-subscriber: subscribe(${label})`);\n let token;\n for (let [t, sub] of this.subscriptions) {\n // search for a subscription with identical label and filter,\n // to avoid making duplicate subscriptions\n if (label === sub.label\n && JSON.stringify(options) === sub.optionsStr) {\n token = t;\n break;\n }\n }\n\n if (!token) {\n token = await this.provisionSubscriptionToken(label, options);\n }\n\n const { callbacks } = this.subscriptions.get(token);\n callbacks.push(callback);\n\n const id = idCounter++;\n this.subscriptionIds.set(id, {\n token, callback,\n });\n\n // Start the keepalive interval\n this.setKeepalive();\n\n return id;\n }\n\n /**\n * @param {SubscriptionId} subscriptionId\n */\n async unsubscribe(subscriptionId) {\n debugging() && console.log(`event-subscriber: unsubscribe)${subscriptionId})`);\n if (!this.subscriptionIds.has(subscriptionId)) {\n throw new Error(`Unknown client subscription id ${subscriptionId}`);\n }\n\n const { token, callback } = this.subscriptionIds.get(subscriptionId);\n this.subscriptionIds.delete(subscriptionId);\n\n if (!this.subscriptions.has(token)) {\n throw new Error(`Unknown subscription token ${token}`);\n }\n\n const subscription = this.subscriptions.get(token);\n subscription.callbacks = subscription.callbacks.filter((cb) => cb !== callback);\n\n if (!subscription.callbacks.length) {\n await this.releaseSubscriptionToken(subscription.label, token);\n this.subscriptions.delete(token);\n }\n\n // If no subscriptions remain, disable the keepalive and allow the Connection\n // to expire\n if (this.subscriptions.size === 0)\n this.setKeepalive(false);\n }\n\n /**\n * De/activate an interval to send keepalives over the event-router connection\n *\n * @param {Boolean} keepalive If true, activate the interval. If false, remove it\n */\n setKeepalive(keepalive = true) {\n if (!keepalive) {\n if (this._keepaliveInterval) {\n clearInterval(this._keepaliveInterval);\n this._keepaliveInterval = null;\n }\n return;\n }\n\n if (!this._keepaliveInterval) {\n this._keepaliveInterval = setInterval(() => {\n this.eventRouterConnection.keepalive();\n }, 3 * 60 * 1000);\n }\n }\n}\n\nexports.EventSubscriber = EventSubscriber;\n\n\n//# sourceURL=webpack:///./src/events/event-subscriber.js?");
5429
+ eval("/**\n * @file events/event-subscriber.js\n * @author Ryan Rossiter <ryan@kingsds.network>\n * @date March 2020\n * \n * This file is the client-side companion to the event-router.\n * It maintains a map of subscription tokens that the event router has provisioned\n * for it, and calls the associated callbacks when the event router emits a new event.\n */\n\nconst protocolV4 = __webpack_require__(/*! dcp/protocol-v4 */ \"./src/protocol-v4/index.js\");\nconst { DcpURL } = __webpack_require__(/*! dcp/common/dcp-url */ \"./src/common/dcp-url.js\");\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\nconst debugging = __webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope('event-subscriber');\n\n/** @typedef {import('./common-types').DcpEvent} DcpEvent */\n/** @typedef {import('./common-types').SubscriptionToken} SubscriptionToken */\n/** @typedef {import('./common-types').EventLabel} EventLabel */\n/** @typedef {import('./common-types').SubscriptionOptions} SubscriptionOptions */\n\n\n/**\n * A container for callback and SubscriptionToken pairs\n * @typedef {Object} CallbackHandle\n * @property {SubscriptionToken} token\n * @property {function} callback\n */\n\n/**\n * @typedef {Object} Subscription\n * @property {EventLabel} label - Event label\n * @property {function[]} callbacks - The callbacks that will be invoked when an event is received for this subscription\n * @property {SubscriptionOptions} options - Additional options\n * @property {string} optionsStr - A string used to memoize stringifying the options object\n */\n\nlet idCounter = 1; // Start at 1 so it's truthy :)\n\nclass EventSubscriber {\n constructor(eventEmitter) {\n this.eventEmitter = eventEmitter;\n\n this.subscriptions = new Map();\n\n /** @type {Interval} Interval to keep the connection alive when no messages\n * are being received */\n this._keepaliveInterval = null;\n\n this.onEventRouterConnectionInterrupted = () => {\n this.openEventRouterConn();\n this.setupEventRouterConnectionEvents();\n this.reestablishSubscriptions();\n }\n\n const ceci = this;\n\n this.eventRouterConnection = null;\n this.openEventRouterConn = function openBankConn()\n {\n ceci.eventRouterConnection = new protocolV4.Connection(dcpConfig.scheduler.services.eventRouter);\n ceci.eventRouterConnection.on('close', ceci.onEventRouterConnectionInterrupted);\n }\n this.openEventRouterConn();\n this.setupEventRouterConnectionEvents();\n }\n\n async close() {\n debugging() && console.log(`event-subscriber: closing EventSubscriber connection`);\n this.eventRouterConnection.off('close', this.onEventRouterConnectionInterrupted);\n await this.eventRouterConnection.close()\n .finally(() => {\n this.subscriptions.clear();\n this.hookedEventRouterConnectionEvents = false;\n });\n }\n\n async reestablishSubscriptions() {\n let subs = this.subscriptions.entries();\n debugging() && console.log(`event-subscriber: reestablishing ${this.subscriptions.size} subscriptions`);\n\n const eventTypes = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\").eventTypes;\n const reliableEvents = [];\n const optionalEvents = [];\n for (let [label, oldToken] of subs) {\n if (oldToken.options)\n {\n // schedmsg special case\n await this.provisionSubscriptionToken(label, oldToken.options);\n }\n else if (eventTypes[label] && eventTypes[label].reliable)\n {\n reliableEvents.push(label);\n }\n else\n {\n optionalEvents.push(label);\n }\n }\n try\n {\n if (reliableEvents.length || optionalEvents.length) {\n const message = {\n reliableEvents,\n optionalEvents,\n options: this.options\n }\n\n await this.provisionSubscriptionTokens(message);\n }\n \n }\n catch (error)\n {\n debugging() && console.warn(`event-subscriber: error establishing subscription (${sub.label}):`, error);\n }\n }\n\n setupEventRouterConnectionEvents() {\n debugging() && console.log(`event-subscriber: setupEventRouterConnectionEvents`)\n this.eventRouterConnection.on('request', (req) => {\n const { operation } = req.payload;\n if (operation === 'event') this.onEventRequest(req);\n else req.respond(new Error(`Unknown EventSubscriber operation: ${operation}`));\n });\n }\n\n async onEventRequest(req) {\n debugging('verbose') && console.log(`event-subscriber: onEventRequest`);\n const { token, event: data } = req.payload.data;\n const event = data.data;\n if (this.subscriptions.has(event.eventName))\n {\n const { token: eventToken } = this.subscriptions.get(event.eventName);\n debugging('verbose') && console.log(`event-subscriber: event: ${event.eventName}`);\n if (eventToken !== token)\n {\n req.respond(new Error(`No subscription registered for token ${token}`));\n }\n else\n {\n const label = event.eventName;\n delete event.eventName;\n if(this.eventEmitter.eventIntercepts)\n {\n if (this.eventEmitter.eventIntercepts[label])\n {\n this.eventEmitter.eventIntercepts[label](event)\n }\n else\n {\n this.eventEmitter.emit(label, event);\n }\n }\n else\n {\n this.eventEmitter.emit(label, event);\n }\n req.respond();\n }\n }\n else\n {\n console.warn(`No subscription registered for label ${event.eventName}`);\n req.respond(new Error(`No subscription registered for token ${event.eventName}`));\n }\n }\n\n /**\n * Registers this subscription with the event router, and saves the returned\n * subscription token into the this.subscriptions map.\n * @param {EventLabel} label\n * @param {SubscriptionOptions} options\n */\n async provisionSubscriptionToken(label, options) {\n debugging('verbose') && console.log(`event-subscriber: provisionSubscriptionToken(${label})`);\n const res = await this.eventRouterConnection.send('subscribe', {\n label, options,\n });\n \n const token = res.payload.token;\n this.subscriptions.set(label, { token, options });\n\n }\n\n async provisionSubscriptionTokens(message) {\n debugging('verbose') && console.log(`event-subscriber: provisionSubscriptionTokens(${message})`);\n const res = await this.eventRouterConnection.send('subscribeMany', message);\n if (!res.success)\n {\n throw new Error(`Failed to subscribe to events, ${res.payload}`);\n }\n \n const tokenEventPairs = res.payload.tokens;\n tokenEventPairs.forEach((tokenAndEvent) => {\n const label = tokenAndEvent.event;\n const token = tokenAndEvent.token;\n this.subscriptions.set(label, { token });\n })\n \n }\n\n /**\n * Unregisters a subscription from the event router\n * @param {EventLabel} label\n * @param {SubscriptionToken} token \n */\n async releaseSubscriptionToken(label, token) {\n debugging('verbose') && console.log(`event-subscriber: releaseSubscriptionToken(${label})`);\n await this.eventRouterConnection.send('unsubscribe', {\n label, token,\n });\n }\n\n /**\n * @param {EventLabel} label - Event label\n * @param {function} callback\n * @param {SubscriptionOptions} options - Additional options\n */\n async subscribeManyEvents(reliableEvents, optionalEvents, options={}) {\n this.options = options;\n const noSubReliableEvents = [];\n reliableEvents.forEach((ev) => {\n for (let [t, sub] of this.subscriptions)\n {\n // search for a subscription with identical label,\n // to avoid making duplicate subscriptions\n if (ev === sub.label)\n {\n return;\n }\n }\n if (!noSubReliableEvents.includes(ev)) {\n noSubReliableEvents.push(ev);\n }\n })\n\n const noSubOptionalEvents = [];\n optionalEvents.forEach((ev) => {\n for (let [t, sub] of this.subscriptions)\n {\n // search for a subscription with identical label,\n // to avoid making duplicate subscriptions\n if (ev === sub.label)\n {\n return;\n }\n }\n if (!noSubOptionalEvents.includes(ev)) {\n noSubOptionalEvents.push(ev);\n }\n })\n\n if (reliableEvents.length || optionalEvents.length)\n {\n const message = {\n reliableEvents: noSubReliableEvents,\n optionalEvents: noSubOptionalEvents,\n options,\n }\n debugging() && console.log('event-subscriber: provisioning:', message);\n await this.provisionSubscriptionTokens(message);\n }\n \n // Start the keepalive interval\n this.setKeepalive();\n }\n \n async subscribe(label, options={}) {\n debugging() && console.log(`event-subscriber: subscribe(${label})`);\n let token;\n for (let [subLabel, t] of this.subscriptions) {\n // search for a subscription with identical label and filter,\n // to avoid making duplicate subscriptions\n if (label === subLabel) {\n token = t;\n break;\n }\n }\n\n if (!token) {\n await this.provisionSubscriptionToken(label, options);\n }\n\n // Start the keepalive interval\n this.setKeepalive();\n }\n\n /**\n * @param {string} label\n */\n async unsubscribe(label) {\n debugging() && console.log(`event-subscriber: unsubscribe)${label})`);\n if (!this.subscriptions.has(label)) {\n throw new Error(`Unknown subscription label ${label}`);\n }\n \n const token = this.subscriptions.get(label)\n await this.releaseSubscriptionToken(label, token);\n this.subscriptions.delete(label);\n\n // If no subscriptions remain, disable the keepalive and allow the Connection\n // to expire\n if (this.subscriptions.size === 0)\n this.setKeepalive(false);\n }\n\n /**\n * De/activate an interval to send keepalives over the event-router connection\n *\n * @param {Boolean} keepalive If true, activate the interval. If false, remove it\n */\n setKeepalive(keepalive = true) {\n if (!keepalive) {\n if (this._keepaliveInterval) {\n clearInterval(this._keepaliveInterval);\n this._keepaliveInterval = null;\n }\n return;\n }\n\n if (!this._keepaliveInterval) {\n this._keepaliveInterval = setInterval(() => {\n this.eventRouterConnection.keepalive();\n }, 3 * 60 * 1000);\n }\n }\n}\n\nexports.EventSubscriber = EventSubscriber;\n\n\n//# sourceURL=webpack:///./src/events/event-subscriber.js?");
5430
5430
 
5431
5431
  /***/ }),
5432
5432
 
@@ -5470,7 +5470,7 @@ eval("/**\n * @file keychain.js - Represents the collection of keystores current
5470
5470
  /*! no static exports found */
5471
5471
  /***/ (function(module, exports) {
5472
5472
 
5473
- eval("/**\n * A Small Modal Class\n * @module Modal\n */\n/* globals Event dcpConfig */\nclass Modal {\n constructor (title, message, callback = false, exitHandler = false, {\n continueLabel = 'Continue',\n cancelLabel = 'Cancel',\n cancelVisible = true\n } = {}) {\n const modal = document.createElement('div')\n modal.className = 'dcp-modal-container-old day'\n modal.innerHTML = `\n <dialog class=\"dcp-modal-content\">\n <div class=\"dcp-modal-header\">\n <h2>${title}<button type=\"button\" class=\"close\">&times;</button></h2>\n ${message ? '<p>' + message + '</p>' : ''}\n </div>\n <div class=\"dcp-modal-loading hidden\">\n <div class='loading'></div>\n </div>\n <form onsubmit='return false' method=\"dialog\">\n <div class=\"dcp-modal-body\"></div>\n <div class=\"dcp-modal-footer ${cancelVisible ? '' : 'centered'}\">\n <button type=\"submit\" class=\"continue green-modal-button\">${continueLabel}</button>\n <button type=\"button\" class=\"cancel green-modal-button\">${cancelLabel}</button>\n </div>\n </form>\n </dialog>`\n\n // To give a reference to do developer who wants to override the form submit.\n // May occur if they want to validate the information in the backend\n // without closing the modal prematurely.\n this.form = modal.querySelector('.dcp-modal-content form')\n this.continueButton = modal.querySelector('.dcp-modal-footer button.continue')\n this.cancelButton = modal.querySelector('.dcp-modal-footer button.cancel')\n this.closeButton = modal.querySelector('.dcp-modal-header .close')\n if (!cancelVisible) {\n this.cancelButton.style.display = 'none'\n }\n\n // To remove the event listener, the reference to the original function\n // added is required.\n this.formSubmitHandler = this.continue.bind(this)\n\n modal.addEventListener('keydown', function (event) {\n event.stopPropagation()\n // 27 is the keycode for the escape key.\n if (event.keyCode === 27) this.close()\n }.bind(this))\n\n this.container = modal\n this.callback = callback\n this.exitHandler = exitHandler\n document.body.appendChild(modal)\n }\n\n changeFormSubmitHandler (newFormSubmitHandler) {\n this.formSubmitHandler = newFormSubmitHandler\n }\n\n /**\n * Validates the form values in the modal and calls the modal's callback\n */\n async continue (event) {\n // To further prevent form submission from trying to redirect from the\n // current page.\n if (event instanceof Event) {\n event.preventDefault()\n }\n let fieldsAreValid = true\n let formElements = this.container.querySelectorAll('.dcp-modal-body select, .dcp-modal-body input, .dcp-modal-body textarea')\n\n const formValues = []\n if (typeof formElements.length === 'undefined') formElements = [formElements]\n // Separate into two loops to enable input validation requiring formValues\n // that come after it. e.g. Two password fields matching.\n for (let i = 0; i < formElements.length; i++) {\n switch (formElements[i].type) {\n case 'file':\n formValues.push(formElements[i])\n break\n case 'checkbox':\n formValues.push(formElements[i].checked)\n break\n default:\n formValues.push(formElements[i].value)\n break\n }\n }\n for (let i = 0; i < formElements.length; i++) {\n if (formElements[i].validation) {\n // Optional fields are allowed to be empty but still can't be wrong if not empty.\n if (!(formElements[i].value === '' && !formElements[i].required)) {\n if (typeof formElements[i].validation === 'function') {\n if (!formElements[i].validation(formValues)) {\n fieldsAreValid = false\n formElements[i].classList.add('is-invalid')\n }\n } else if (!formElements[i].validation.test(formElements[i].value)) {\n fieldsAreValid = false\n formElements[i].classList.add('is-invalid')\n }\n }\n }\n }\n\n if (!fieldsAreValid) return\n\n this.loading()\n if (typeof this.callback === 'function') {\n try {\n return this.callback(formValues)\n } catch (error) {\n console.error('Unexpected error in modal.continue:', error);\n return this.close(false)\n }\n }\n this.close(true)\n }\n\n loading () {\n this.container.querySelector('.dcp-modal-loading').classList.remove('hidden')\n this.container.querySelector('.dcp-modal-body').classList.add('hidden')\n this.container.querySelector('.dcp-modal-footer').classList.add('hidden')\n }\n\n open () {\n this.form.addEventListener('submit', async (event) => {\n const success = await this.formSubmitHandler(event)\n if (success === false) {\n return\n }\n this.close(true)\n })\n // When the user clicks on <span> (x), close the modal\n this.closeButton.addEventListener('click', this.close.bind(this))\n this.cancelButton.addEventListener('click', this.close.bind(this))\n\n // Prevent lingering outlines after clicking some form elements.\n this.container.querySelectorAll('.dcp-modal-body button, .dcp-modal-body input[type=\"checkbox\"]').forEach(element => {\n element.addEventListener('click', () => {\n element.blur()\n })\n })\n\n // Show the modal.\n this.container.style.display = 'block'\n\n const formElements = this.container.querySelectorAll('.dcp-modal-body select, .dcp-modal-body input')\n if (formElements.length) {\n formElements[0].focus()\n if (formElements[0].type === 'text') {\n formElements[0].select()\n }\n for (const el of formElements) {\n if (el.realType) {\n el.type = el.realType\n }\n }\n } else {\n // With no form elements to allow for form submission on enter, focus the\n // continue button.\n this.container.querySelector('.dcp-modal-footer button.continue').focus()\n }\n } // TODO: This should return a promise with the action resolving it\n\n /**\n * Shows the modal and returns a promise of the result of the modal (e.g. was\n * it closed, did its action succeed?)\n */\n showModal () {\n return new Promise((resolve, reject) => {\n this.form.addEventListener('submit', handleContinue.bind(this))\n this.cancelButton.addEventListener('click', handleCancel.bind(this))\n this.closeButton.addEventListener('click', handleCancel.bind(this))\n\n // Prevent lingering outlines after clicking some form elements.\n this.container.querySelectorAll('.dcp-modal-body button, .dcp-modal-body input[type=\"checkbox\"]').forEach(element => {\n element.addEventListener('click', () => {\n element.blur()\n })\n })\n\n // Show the modal.\n this.container.style.display = 'block'\n\n const formElements = this.container.querySelectorAll('.dcp-modal-body select, .dcp-modal-body input')\n if (formElements.length) {\n formElements[0].focus()\n if (formElements[0].type === 'text') {\n formElements[0].select()\n }\n for (const el of formElements) {\n if (el.realType) {\n el.type = el.realType\n }\n }\n } else {\n // With no form elements to allow for form submission on enter, focus the\n // continue button.\n this.continueButton.focus()\n }\n\n async function handleContinue (event) {\n let result\n try {\n result = await this.formSubmitHandler(event)\n } catch (error) {\n reject(error)\n }\n this.close(true)\n resolve(result)\n }\n\n async function handleCancel () {\n let result\n try {\n result = await this.close()\n } catch (error) {\n reject(error)\n }\n resolve(result)\n }\n })\n }\n\n close (success = false) {\n this.container.style.display = 'none'\n if (this.container.parentNode) {\n this.container.parentNode.removeChild(this.container)\n }\n\n // @todo this needs to remove eventlisteners to prevent memory leaks\n\n if ((success !== true) && typeof this.exitHandler === 'function') {\n return this.exitHandler(this)\n }\n }\n\n /**\n * Adds different form elements to the modal depending on the case.\n *\n * @param {*} elements - The properties of the form elements to add.\n * @returns {HTMLElement} The input form elements.\n */\n addFormElement (...elements) {\n const body = this.container.querySelector('.dcp-modal-body')\n const inputElements = []\n let label\n for (let i = 0; i < elements.length; i++) {\n let row = document.createElement('div')\n row.className = 'row'\n\n let col, input\n switch (elements[i].type) {\n case 'button':\n col = document.createElement('div')\n col.className = 'col-md-12'\n\n input = document.createElement('button')\n input.innerHTML = elements[i].label\n input.type = 'button'\n input.classList.add('green-modal-button')\n if (!elements[i].onclick) {\n throw new Error('A button in the modal body should have an on click event handler.')\n }\n input.addEventListener('click', elements[i].onclick)\n\n col.appendChild(input)\n row.appendChild(col)\n break\n case 'textarea':\n col = document.createElement('div')\n col.className = 'col-md-12'\n\n input = document.createElement('textarea')\n input.className = 'text-input-field form-control'\n if (elements[i].placeholder) input.placeholder = elements[i].placeholder\n\n col.appendChild(input)\n row.appendChild(col)\n break\n case 'text':\n case 'email':\n case 'number':\n case 'password': {\n const inputCol = document.createElement('div')\n\n input = document.createElement('input')\n input.type = elements[i].type\n input.validation = elements[i].validation\n input.autocomplete = elements[i].autocomplete || (elements[i].type === 'password' ? 'off' : 'on')\n input.className = 'text-input-field form-control'\n\n // Adding bootstraps custom feedback styles.\n let invalidFeedback = null\n if (elements[i].invalidFeedback) {\n invalidFeedback = document.createElement('div')\n invalidFeedback.className = 'invalid-feedback'\n invalidFeedback.innerText = elements[i].invalidFeedback\n }\n\n if (elements[i].type === 'password') {\n elements[i].realType = 'password'\n }\n\n if (elements[i].label) {\n const labelCol = document.createElement('div')\n label = document.createElement('label')\n label.innerText = elements[i].label\n const inputId = 'dcp-modal-input-' + this.container.querySelectorAll('input[type=\"text\"], input[type=\"email\"], input[type=\"number\"], input[type=\"password\"]').length\n label.setAttribute('for', inputId)\n input.id = inputId\n labelCol.classList.add('col-md-6', 'label-column')\n labelCol.appendChild(label)\n row.appendChild(labelCol)\n inputCol.className = 'col-md-6'\n } else {\n inputCol.className = 'col-md-12'\n }\n\n inputCol.appendChild(input)\n if (invalidFeedback !== null) {\n inputCol.appendChild(invalidFeedback)\n }\n row.appendChild(inputCol)\n break\n }\n case 'select':\n col = document.createElement('div')\n col.className = 'col-md-4'\n\n label = document.createElement('span')\n label.innerText = elements[i].label\n\n col.appendChild(label)\n row.appendChild(col)\n\n col = document.createElement('div')\n col.className = 'col-md-8'\n\n input = document.createElement('select')\n\n col.appendChild(input)\n row.appendChild(col)\n break\n case 'checkbox': {\n row.classList.add('checkbox-row')\n const checkboxLabelCol = document.createElement('div')\n checkboxLabelCol.classList.add('label-column', 'checkbox-label-column')\n\n label = document.createElement('label')\n label.innerText = elements[i].label\n label.for = 'dcp-checkbox-input-' + this.container.querySelectorAll('input[type=\"checkbox\"]').length\n label.setAttribute('for', label.for)\n label.className = 'checkbox-label'\n\n checkboxLabelCol.appendChild(label)\n\n const checkboxCol = document.createElement('div')\n checkboxCol.classList.add('checkbox-column')\n\n input = document.createElement('input')\n input.type = 'checkbox'\n input.id = label.for\n if (elements[i].checked) {\n input.checked = true\n }\n\n checkboxCol.appendChild(input)\n\n if (elements[i].labelToTheRightOfCheckbox) {\n checkboxCol.classList.add('col-md-5')\n row.appendChild(checkboxCol)\n checkboxLabelCol.classList.add('col-md-7')\n row.appendChild(checkboxLabelCol)\n } else {\n checkboxLabelCol.classList.add('col-md-6')\n checkboxCol.classList.add('col-md-6')\n row.appendChild(checkboxLabelCol)\n row.appendChild(checkboxCol)\n }\n break\n }\n case 'file':\n [input, row] = this.addFileInput(elements[i], input, row)\n break\n case 'label':\n row.classList.add('label-row')\n label = document.createElement('label')\n label.innerText = elements[i].label\n row.appendChild(label)\n break\n }\n\n // Copy other possibly specified element properties:\n const inputPropertyNames = ['title', 'inputmode', 'value', 'minLength', 'maxLength', 'size', 'required', 'pattern', 'min', 'max', 'step', 'placeholder', 'accept', 'multiple', 'id', 'onkeypress', 'oninput', 'for', 'readonly', 'autocomplete']\n for (const propertyName of inputPropertyNames) {\n if (Object.prototype.hasOwnProperty.call(elements[i], propertyName)) {\n if (propertyName === 'for' && !label.hasAttribute(propertyName)) {\n label.setAttribute(propertyName, elements[i][propertyName])\n }\n if (propertyName.startsWith('on')) {\n input.addEventListener(propertyName.slice(2), elements[i][propertyName])\n } else {\n input.setAttribute(propertyName, elements[i][propertyName])\n }\n }\n }\n\n inputElements.push(input)\n body.appendChild(row)\n }\n\n if (inputElements.length === 1) return inputElements[0]\n else return inputElements\n }\n\n /**\n * Adds a drag and drop file form element to the modal.\n *\n * @param {*} fileInputProperties - An object specifying some of the\n * properties of the file input element.\n * @param {*} fileInput - Placeholders to help create the file\n * input.\n * @param {HTMLDivElement} row - Placeholders to help create the file\n * input.\n */\n addFileInput (fileInputProperties, fileInput, row) {\n // Adding the upload label.\n const uploadLabel = document.createElement('label')\n uploadLabel.innerText = fileInputProperties.label\n row.appendChild(uploadLabel)\n const body = this.container.querySelector('.dcp-modal-body')\n body.appendChild(row)\n const fileSelectionRow = document.createElement('div')\n fileSelectionRow.id = 'file-selection-row'\n\n // Adding the drag and drop file upload input.\n const dropContainer = document.createElement('div')\n dropContainer.id = 'drop-container'\n\n // Adding an image of a wallet\n const imageContainer = document.createElement('div')\n imageContainer.id = 'image-container'\n const walletImage = document.createElement('span')\n walletImage.classList.add('fas', 'fa-wallet')\n imageContainer.appendChild(walletImage)\n\n // Adding some text prompts\n const dropMessage = document.createElement('span')\n dropMessage.innerText = 'Drop a keystore file here'\n const orMessage = document.createElement('span')\n orMessage.innerText = 'or'\n\n // Adding the manual file input element (hiding the default one)\n const fileInputContainer = document.createElement('div')\n const fileInputLabel = document.createElement('label')\n // Linking the label to the file input so that clicking on the label\n // activates the file input.\n fileInputLabel.setAttribute('for', 'file-input')\n fileInputLabel.innerText = 'Browse'\n fileInput = document.createElement('input')\n fileInput.type = fileInputProperties.type\n fileInput.id = 'file-input'\n // To remove the lingering outline after selecting the file.\n fileInput.addEventListener('click', () => {\n fileInput.blur()\n })\n fileInputContainer.append(fileInput, fileInputLabel)\n\n // Creating the final row element to append to the modal body.\n dropContainer.append(imageContainer, dropMessage, orMessage, fileInputContainer)\n fileSelectionRow.appendChild(dropContainer)\n\n // Adding functionality to the drag and drop file input.\n dropContainer.addEventListener('drop', selectDroppedFile.bind(this))\n dropContainer.addEventListener('drop', unhighlightDropArea)\n // Prevent file from being opened by the browser.\n dropContainer.ondragover = highlightDropArea\n dropContainer.ondragenter = highlightDropArea\n dropContainer.ondragleave = unhighlightDropArea\n\n fileInput.addEventListener('change', handleFileChange)\n\n const fileNamePlaceholder = document.createElement('center')\n fileNamePlaceholder.id = 'file-name-placeholder'\n fileNamePlaceholder.className = 'row'\n fileNamePlaceholder.innerText = ''\n fileSelectionRow.appendChild(fileNamePlaceholder)\n fileNamePlaceholder.classList.add('hidden')\n\n // Check if the continue button is invalid on the keystore upload modal and\n // click it if it should no longer be invalid.\n this.continueButton.addEventListener('invalid', () => {\n const fileFormElements = this.container.querySelectorAll('.dcp-modal-body input[type=\"file\"], .dcp-modal-body input[type=\"text\"]')\n const filledInFileFormElements = Array.from(fileFormElements).filter(fileFormElement => fileFormElement.value !== '')\n if (fileFormElements.length !== 0 && filledInFileFormElements.length !== 0) {\n this.continueButton.setCustomValidity('')\n // Clicking instead of dispatching a submit event to ensure other form validation is used before submitting the form.\n this.continueButton.click()\n }\n })\n\n return [fileInput, fileSelectionRow]\n\n /**\n * Checks that the dropped items contain only a single keystore file.\n * If valid, sets the file input's value to the dropped file.\n * @param {DragEvent} event - Contains the files dropped.\n */\n function selectDroppedFile (event) {\n // Prevent file from being opened.\n event.preventDefault()\n\n // Check if only one file was dropped.\n const wasOneFileDropped = event.dataTransfer.items.length === 1 ||\n event.dataTransfer.files.length === 1\n updateFileSelectionStatus(wasOneFileDropped)\n if (!wasOneFileDropped) {\n fileInput.setCustomValidity('Only one file can be uploaded.')\n fileInput.reportValidity()\n return\n } else {\n fileInput.setCustomValidity('')\n }\n\n // Now to use the DataTransfer interface to access the file(s), setting\n // the value of the file input.\n const file = event.dataTransfer.files[0]\n\n if (checkFileExtension(file)) {\n fileInput.files = event.dataTransfer.files\n fileInput.dispatchEvent(new Event('change'))\n }\n }\n\n function handleFileChange () {\n if (checkFileExtension(this.files[0]) && this.files.length === 1) {\n fileNamePlaceholder.innerText = `Selected File: ${this.files[0].name}`\n updateFileSelectionStatus(true)\n // Invoke a callback if additional functionality is required.\n if (typeof fileInputProperties.callback === 'function') {\n fileInputProperties.callback(this.files[0])\n }\n }\n }\n\n /**\n * Checks if the file extension on the inputted file is correct.\n * @param {File} file - The file to check\n * @returns {boolean} True if the file extension is valid, false otherwise.\n */\n function checkFileExtension (file) {\n // If there's no restriction, return true.\n if (!fileInputProperties.extension) {\n return true\n }\n const fileExtension = file.name.split('.').pop()\n const isValidExtension = fileExtension === fileInputProperties.extension\n updateFileSelectionStatus(isValidExtension)\n if (!isValidExtension) {\n fileInput.setCustomValidity(`Only a .${fileInputProperties.extension} file can be uploaded.`)\n fileInput.reportValidity()\n fileNamePlaceholder.classList.add('hidden')\n } else {\n fileInput.setCustomValidity('')\n }\n return isValidExtension\n }\n\n /**\n * Updates the file input to reflect the validity of the current file\n * selection.\n * @param {boolean} isValidFileSelection - True if a single .keystore file\n * was selected. False otherwise.\n */\n function updateFileSelectionStatus (isValidFileSelection) {\n imageContainer.innerHTML = ''\n const statusImage = document.createElement('span')\n statusImage.classList.add('fas', isValidFileSelection ? 'fa-check' : 'fa-times')\n statusImage.style.color = isValidFileSelection ? 'green' : 'red'\n imageContainer.appendChild(statusImage)\n\n if (!isValidFileSelection) {\n fileInput.value = null\n fileNamePlaceholder.classList.add('hidden')\n } else {\n fileNamePlaceholder.classList.remove('hidden')\n }\n\n // If the modal contains a password field for a keystore file, change its\n // visibility.\n const walletPasswordInputContainer = document.querySelector('.dcp-modal-body input[type=\"password\"]').parentElement.parentElement\n if (walletPasswordInputContainer) {\n if (isValidFileSelection) {\n walletPasswordInputContainer.classList.remove('hidden')\n const walletPasswordInput = document.querySelector('.dcp-modal-body input[type=\"password\"]')\n walletPasswordInput.focus()\n } else {\n walletPasswordInputContainer.classList.add('hidden')\n }\n }\n }\n\n function highlightDropArea (event) {\n event.preventDefault()\n this.classList.add('highlight')\n }\n\n function unhighlightDropArea (event) {\n event.preventDefault()\n this.classList.remove('highlight')\n }\n }\n\n /**\n * Sets up a custom tooltip to pop up when the passwords do not match, but are\n * valid otherwise.\n */\n addFormValidationForPasswordConfirmation () {\n const [newPassword, confirmPassword] = document.querySelectorAll('.dcp-modal-body input[type=\"password\"]')\n if (!newPassword || !confirmPassword) {\n throw Error('New Password field and Confirm Password fields not present.')\n }\n\n newPassword.addEventListener('input', checkMatchingPasswords)\n confirmPassword.addEventListener('input', checkMatchingPasswords)\n\n function checkMatchingPasswords () {\n if (newPassword.value !== confirmPassword.value &&\n newPassword.validity.valid &&\n confirmPassword.validity.valid) {\n newPassword.setCustomValidity('Both passwords must match.')\n } else if (newPassword.value === confirmPassword.value ||\n newPassword.validity.tooShort ||\n newPassword.validity.patternMismatch ||\n newPassword.validity.valueMissing ||\n confirmPassword.validity.tooShort ||\n confirmPassword.validity.patternMismatch ||\n confirmPassword.validity.valueMissing) {\n // If the passwords fields match or have become invalidated some other\n // way again, reset the custom message.\n newPassword.setCustomValidity('')\n }\n }\n }\n\n updateInvalidEmailMessage() {\n const email = document.querySelector('.dcp-modal-body input[id=\"email\"')\n if (!email){\n throw Error(\"Email field not present\")\n }\n email.addEventListener('input', checkValidEmail);\n function checkValidEmail() {\n if (!email.validity.patternMismatch &&\n !email.validity.valueMissing) {\n email.setCustomValidity('')\n } else {\n email.setCustomValidity(\"Enter a valid email address.\")\n }\n\n }\n }\n\n /**\n * Adds message(s) to the modal's body.\n * @param {string} messages - The message(s) to add to the modal's body.\n * @returns Paragraph element(s) containing the message(s) added to the\n * modal's body.\n */\n addMessage (...messages) {\n const elements = []\n const body = this.container.querySelector('.dcp-modal-body')\n for (let i = 0; i < messages.length; i++) {\n const row = document.createElement('div')\n row.className = 'row'\n\n const paragraph = document.createElement('p')\n paragraph.innerHTML = messages[i]\n paragraph.classList.add('message')\n row.appendChild(paragraph)\n body.appendChild(row)\n\n elements.push(paragraph)\n }\n\n if (elements.length === 1) return elements[0]\n else return elements\n }\n\n addHorizontalRule () {\n const body = this.container.querySelector('.dcp-modal-body')\n body.appendChild(document.createElement('hr'))\n }\n\n // Does what it says. Still ill advised to use unless you have to.\n addCustomHTML (htmlStr, browseCallback) {\n const elements = []\n const body = this.container.querySelector('.dcp-modal-body')\n body.innerHTML += htmlStr\n body.querySelector('#browse-button').addEventListener('click', browseCallback.bind(this, this))\n\n if (elements.length === 1) return elements[0]\n else return elements\n }\n\n addButton (...buttons) {\n const elements = []\n const body = this.container.querySelector('.dcp-modal-body')\n for (let i = 0; i < buttons.length; i++) {\n const row = document.createElement('div')\n row.className = 'row'\n\n let col = document.createElement('div')\n col.className = 'col-md-4'\n\n const description = document.createElement('span')\n description.innerText = buttons[i].description\n\n col.appendChild(description)\n row.appendChild(col)\n\n col = document.createElement('div')\n col.className = 'col-md-8'\n\n const button = document.createElement('button')\n button.innerText = buttons[i].label\n button.addEventListener('click', buttons[i].callback.bind(this, this))\n\n elements.push(button)\n\n col.appendChild(button)\n row.appendChild(col)\n\n body.appendChild(row)\n }\n\n if (elements.length === 1) return elements[0]\n else return elements\n }\n}\n\nexports.Modal = Modal;\n\n// Inject our special stylesheet from dcp-client only if we're on the portal webpage.\nif (typeof window !== 'undefined' && typeof document !== 'undefined' && dcpConfig.portal.location.hostname === window.location.hostname) {\n // <link rel='stylesheet' href='/css/dashboard.css'>\n const stylesheet = document.createElement('link')\n stylesheet.rel = 'stylesheet'\n // Needed for the duplicate check done later.\n stylesheet.id = 'dcp-modal-styles'\n\n const dcpClientBundle = document.getElementById('_dcp_client_bundle')\n let src\n if (dcpClientBundle) {\n src = dcpClientBundle.src.replace('dcp-client-bundle.js', 'dcp-modal-style.css')\n } else {\n src = dcpConfig.portal.location.href + 'dcp-client/dist/dcp-modal-style.css'\n }\n\n stylesheet.href = src\n // If the style was injected before, don't inject it again.\n // Could occur when loading a file that imports Modal.js and loading\n // comput.min.js in the same HTML file.\n if (document.getElementById(stylesheet.id) === null) {\n document.getElementsByTagName('head')[0].appendChild(stylesheet)\n }\n\n if (typeof {\"version\":\"dd98e423ca01eaeeba22e0bd0d948e358ffc7b43\",\"branch\":\"release\",\"dcpClient\":{\"version\":\"4.1.11\",\"from\":\"git+ssh://git@gitlab.com/Distributed-Compute-Protocol/dcp-client.git#release\",\"resolved\":\"git+ssh://git@gitlab.com/Distributed-Compute-Protocol/dcp-client.git#9275023e3d51fbc2437111118c8d3bbb73bfebc1\"},\"built\":\"Mon Sep 20 2021 11:03:08 GMT-0400 (Eastern Daylight Time)\",\"config\":{\"generated\":\"Mon 20 Sep 2021 11:03:07 AM EDT by erose on lorge\",\"build\":\"debug\"},\"webpack\":\"4.46.0\",\"node\":\"v12.22.6\"} !== 'undefined' && typeof window.Modal === 'undefined') {\n window.Modal = Modal\n }\n}\n\n\n//# sourceURL=webpack:///./src/protocol-v3/modal.js?");
5473
+ eval("/**\n * A Small Modal Class\n * @module Modal\n */\n/* globals Event dcpConfig */\nclass Modal {\n constructor (title, message, callback = false, exitHandler = false, {\n continueLabel = 'Continue',\n cancelLabel = 'Cancel',\n cancelVisible = true\n } = {}) {\n const modal = document.createElement('div')\n modal.className = 'dcp-modal-container-old day'\n modal.innerHTML = `\n <dialog class=\"dcp-modal-content\">\n <div class=\"dcp-modal-header\">\n <h2>${title}<button type=\"button\" class=\"close\">&times;</button></h2>\n ${message ? '<p>' + message + '</p>' : ''}\n </div>\n <div class=\"dcp-modal-loading hidden\">\n <div class='loading'></div>\n </div>\n <form onsubmit='return false' method=\"dialog\">\n <div class=\"dcp-modal-body\"></div>\n <div class=\"dcp-modal-footer ${cancelVisible ? '' : 'centered'}\">\n <button type=\"submit\" class=\"continue green-modal-button\">${continueLabel}</button>\n <button type=\"button\" class=\"cancel green-modal-button\">${cancelLabel}</button>\n </div>\n </form>\n </dialog>`\n\n // To give a reference to do developer who wants to override the form submit.\n // May occur if they want to validate the information in the backend\n // without closing the modal prematurely.\n this.form = modal.querySelector('.dcp-modal-content form')\n this.continueButton = modal.querySelector('.dcp-modal-footer button.continue')\n this.cancelButton = modal.querySelector('.dcp-modal-footer button.cancel')\n this.closeButton = modal.querySelector('.dcp-modal-header .close')\n if (!cancelVisible) {\n this.cancelButton.style.display = 'none'\n }\n\n // To remove the event listener, the reference to the original function\n // added is required.\n this.formSubmitHandler = this.continue.bind(this)\n\n modal.addEventListener('keydown', function (event) {\n event.stopPropagation()\n // 27 is the keycode for the escape key.\n if (event.keyCode === 27) this.close()\n }.bind(this))\n\n this.container = modal\n this.callback = callback\n this.exitHandler = exitHandler\n document.body.appendChild(modal)\n }\n\n changeFormSubmitHandler (newFormSubmitHandler) {\n this.formSubmitHandler = newFormSubmitHandler\n }\n\n /**\n * Validates the form values in the modal and calls the modal's callback\n */\n async continue (event) {\n // To further prevent form submission from trying to redirect from the\n // current page.\n if (event instanceof Event) {\n event.preventDefault()\n }\n let fieldsAreValid = true\n let formElements = this.container.querySelectorAll('.dcp-modal-body select, .dcp-modal-body input, .dcp-modal-body textarea')\n\n const formValues = []\n if (typeof formElements.length === 'undefined') formElements = [formElements]\n // Separate into two loops to enable input validation requiring formValues\n // that come after it. e.g. Two password fields matching.\n for (let i = 0; i < formElements.length; i++) {\n switch (formElements[i].type) {\n case 'file':\n formValues.push(formElements[i])\n break\n case 'checkbox':\n formValues.push(formElements[i].checked)\n break\n default:\n formValues.push(formElements[i].value)\n break\n }\n }\n for (let i = 0; i < formElements.length; i++) {\n if (formElements[i].validation) {\n // Optional fields are allowed to be empty but still can't be wrong if not empty.\n if (!(formElements[i].value === '' && !formElements[i].required)) {\n if (typeof formElements[i].validation === 'function') {\n if (!formElements[i].validation(formValues)) {\n fieldsAreValid = false\n formElements[i].classList.add('is-invalid')\n }\n } else if (!formElements[i].validation.test(formElements[i].value)) {\n fieldsAreValid = false\n formElements[i].classList.add('is-invalid')\n }\n }\n }\n }\n\n if (!fieldsAreValid) return\n\n this.loading()\n if (typeof this.callback === 'function') {\n try {\n return this.callback(formValues)\n } catch (error) {\n console.error('Unexpected error in modal.continue:', error);\n return this.close(false)\n }\n }\n this.close(true)\n }\n\n loading () {\n this.container.querySelector('.dcp-modal-loading').classList.remove('hidden')\n this.container.querySelector('.dcp-modal-body').classList.add('hidden')\n this.container.querySelector('.dcp-modal-footer').classList.add('hidden')\n }\n\n open () {\n this.form.addEventListener('submit', async (event) => {\n const success = await this.formSubmitHandler(event)\n if (success === false) {\n return\n }\n this.close(true)\n })\n // When the user clicks on <span> (x), close the modal\n this.closeButton.addEventListener('click', this.close.bind(this))\n this.cancelButton.addEventListener('click', this.close.bind(this))\n\n // Prevent lingering outlines after clicking some form elements.\n this.container.querySelectorAll('.dcp-modal-body button, .dcp-modal-body input[type=\"checkbox\"]').forEach(element => {\n element.addEventListener('click', () => {\n element.blur()\n })\n })\n\n // Show the modal.\n this.container.style.display = 'block'\n\n const formElements = this.container.querySelectorAll('.dcp-modal-body select, .dcp-modal-body input')\n if (formElements.length) {\n formElements[0].focus()\n if (formElements[0].type === 'text') {\n formElements[0].select()\n }\n for (const el of formElements) {\n if (el.realType) {\n el.type = el.realType\n }\n }\n } else {\n // With no form elements to allow for form submission on enter, focus the\n // continue button.\n this.container.querySelector('.dcp-modal-footer button.continue').focus()\n }\n } // TODO: This should return a promise with the action resolving it\n\n /**\n * Shows the modal and returns a promise of the result of the modal (e.g. was\n * it closed, did its action succeed?)\n */\n showModal () {\n return new Promise((resolve, reject) => {\n this.form.addEventListener('submit', handleContinue.bind(this))\n this.cancelButton.addEventListener('click', handleCancel.bind(this))\n this.closeButton.addEventListener('click', handleCancel.bind(this))\n\n // Prevent lingering outlines after clicking some form elements.\n this.container.querySelectorAll('.dcp-modal-body button, .dcp-modal-body input[type=\"checkbox\"]').forEach(element => {\n element.addEventListener('click', () => {\n element.blur()\n })\n })\n\n // Show the modal.\n this.container.style.display = 'block'\n\n const formElements = this.container.querySelectorAll('.dcp-modal-body select, .dcp-modal-body input')\n if (formElements.length) {\n formElements[0].focus()\n if (formElements[0].type === 'text') {\n formElements[0].select()\n }\n for (const el of formElements) {\n if (el.realType) {\n el.type = el.realType\n }\n }\n } else {\n // With no form elements to allow for form submission on enter, focus the\n // continue button.\n this.continueButton.focus()\n }\n\n async function handleContinue (event) {\n let result\n try {\n result = await this.formSubmitHandler(event)\n } catch (error) {\n reject(error)\n }\n this.close(true)\n resolve(result)\n }\n\n async function handleCancel () {\n let result\n try {\n result = await this.close()\n } catch (error) {\n reject(error)\n }\n resolve(result)\n }\n })\n }\n\n close (success = false) {\n this.container.style.display = 'none'\n if (this.container.parentNode) {\n this.container.parentNode.removeChild(this.container)\n }\n\n // @todo this needs to remove eventlisteners to prevent memory leaks\n\n if ((success !== true) && typeof this.exitHandler === 'function') {\n return this.exitHandler(this)\n }\n }\n\n /**\n * Adds different form elements to the modal depending on the case.\n *\n * @param {*} elements - The properties of the form elements to add.\n * @returns {HTMLElement} The input form elements.\n */\n addFormElement (...elements) {\n const body = this.container.querySelector('.dcp-modal-body')\n const inputElements = []\n let label\n for (let i = 0; i < elements.length; i++) {\n let row = document.createElement('div')\n row.className = 'row'\n\n let col, input\n switch (elements[i].type) {\n case 'button':\n col = document.createElement('div')\n col.className = 'col-md-12'\n\n input = document.createElement('button')\n input.innerHTML = elements[i].label\n input.type = 'button'\n input.classList.add('green-modal-button')\n if (!elements[i].onclick) {\n throw new Error('A button in the modal body should have an on click event handler.')\n }\n input.addEventListener('click', elements[i].onclick)\n\n col.appendChild(input)\n row.appendChild(col)\n break\n case 'textarea':\n col = document.createElement('div')\n col.className = 'col-md-12'\n\n input = document.createElement('textarea')\n input.className = 'text-input-field form-control'\n if (elements[i].placeholder) input.placeholder = elements[i].placeholder\n\n col.appendChild(input)\n row.appendChild(col)\n break\n case 'text':\n case 'email':\n case 'number':\n case 'password': {\n const inputCol = document.createElement('div')\n\n input = document.createElement('input')\n input.type = elements[i].type\n input.validation = elements[i].validation\n input.autocomplete = elements[i].autocomplete || (elements[i].type === 'password' ? 'off' : 'on')\n input.className = 'text-input-field form-control'\n\n // Adding bootstraps custom feedback styles.\n let invalidFeedback = null\n if (elements[i].invalidFeedback) {\n invalidFeedback = document.createElement('div')\n invalidFeedback.className = 'invalid-feedback'\n invalidFeedback.innerText = elements[i].invalidFeedback\n }\n\n if (elements[i].type === 'password') {\n elements[i].realType = 'password'\n }\n\n if (elements[i].label) {\n const labelCol = document.createElement('div')\n label = document.createElement('label')\n label.innerText = elements[i].label\n const inputId = 'dcp-modal-input-' + this.container.querySelectorAll('input[type=\"text\"], input[type=\"email\"], input[type=\"number\"], input[type=\"password\"]').length\n label.setAttribute('for', inputId)\n input.id = inputId\n labelCol.classList.add('col-md-6', 'label-column')\n labelCol.appendChild(label)\n row.appendChild(labelCol)\n inputCol.className = 'col-md-6'\n } else {\n inputCol.className = 'col-md-12'\n }\n\n inputCol.appendChild(input)\n if (invalidFeedback !== null) {\n inputCol.appendChild(invalidFeedback)\n }\n row.appendChild(inputCol)\n break\n }\n case 'select':\n col = document.createElement('div')\n col.className = 'col-md-4'\n\n label = document.createElement('span')\n label.innerText = elements[i].label\n\n col.appendChild(label)\n row.appendChild(col)\n\n col = document.createElement('div')\n col.className = 'col-md-8'\n\n input = document.createElement('select')\n\n col.appendChild(input)\n row.appendChild(col)\n break\n case 'checkbox': {\n row.classList.add('checkbox-row')\n const checkboxLabelCol = document.createElement('div')\n checkboxLabelCol.classList.add('label-column', 'checkbox-label-column')\n\n label = document.createElement('label')\n label.innerText = elements[i].label\n label.for = 'dcp-checkbox-input-' + this.container.querySelectorAll('input[type=\"checkbox\"]').length\n label.setAttribute('for', label.for)\n label.className = 'checkbox-label'\n\n checkboxLabelCol.appendChild(label)\n\n const checkboxCol = document.createElement('div')\n checkboxCol.classList.add('checkbox-column')\n\n input = document.createElement('input')\n input.type = 'checkbox'\n input.id = label.for\n if (elements[i].checked) {\n input.checked = true\n }\n\n checkboxCol.appendChild(input)\n\n if (elements[i].labelToTheRightOfCheckbox) {\n checkboxCol.classList.add('col-md-5')\n row.appendChild(checkboxCol)\n checkboxLabelCol.classList.add('col-md-7')\n row.appendChild(checkboxLabelCol)\n } else {\n checkboxLabelCol.classList.add('col-md-6')\n checkboxCol.classList.add('col-md-6')\n row.appendChild(checkboxLabelCol)\n row.appendChild(checkboxCol)\n }\n break\n }\n case 'file':\n [input, row] = this.addFileInput(elements[i], input, row)\n break\n case 'label':\n row.classList.add('label-row')\n label = document.createElement('label')\n label.innerText = elements[i].label\n row.appendChild(label)\n break\n }\n\n // Copy other possibly specified element properties:\n const inputPropertyNames = ['title', 'inputmode', 'value', 'minLength', 'maxLength', 'size', 'required', 'pattern', 'min', 'max', 'step', 'placeholder', 'accept', 'multiple', 'id', 'onkeypress', 'oninput', 'for', 'readonly', 'autocomplete']\n for (const propertyName of inputPropertyNames) {\n if (Object.prototype.hasOwnProperty.call(elements[i], propertyName)) {\n if (propertyName === 'for' && !label.hasAttribute(propertyName)) {\n label.setAttribute(propertyName, elements[i][propertyName])\n }\n if (propertyName.startsWith('on')) {\n input.addEventListener(propertyName.slice(2), elements[i][propertyName])\n } else {\n input.setAttribute(propertyName, elements[i][propertyName])\n }\n }\n }\n\n inputElements.push(input)\n body.appendChild(row)\n }\n\n if (inputElements.length === 1) return inputElements[0]\n else return inputElements\n }\n\n /**\n * Adds a drag and drop file form element to the modal.\n *\n * @param {*} fileInputProperties - An object specifying some of the\n * properties of the file input element.\n * @param {*} fileInput - Placeholders to help create the file\n * input.\n * @param {HTMLDivElement} row - Placeholders to help create the file\n * input.\n */\n addFileInput (fileInputProperties, fileInput, row) {\n // Adding the upload label.\n const uploadLabel = document.createElement('label')\n uploadLabel.innerText = fileInputProperties.label\n row.appendChild(uploadLabel)\n const body = this.container.querySelector('.dcp-modal-body')\n body.appendChild(row)\n const fileSelectionRow = document.createElement('div')\n fileSelectionRow.id = 'file-selection-row'\n\n // Adding the drag and drop file upload input.\n const dropContainer = document.createElement('div')\n dropContainer.id = 'drop-container'\n\n // Adding an image of a wallet\n const imageContainer = document.createElement('div')\n imageContainer.id = 'image-container'\n const walletImage = document.createElement('span')\n walletImage.classList.add('fas', 'fa-wallet')\n imageContainer.appendChild(walletImage)\n\n // Adding some text prompts\n const dropMessage = document.createElement('span')\n dropMessage.innerText = 'Drop a keystore file here'\n const orMessage = document.createElement('span')\n orMessage.innerText = 'or'\n\n // Adding the manual file input element (hiding the default one)\n const fileInputContainer = document.createElement('div')\n const fileInputLabel = document.createElement('label')\n // Linking the label to the file input so that clicking on the label\n // activates the file input.\n fileInputLabel.setAttribute('for', 'file-input')\n fileInputLabel.innerText = 'Browse'\n fileInput = document.createElement('input')\n fileInput.type = fileInputProperties.type\n fileInput.id = 'file-input'\n // To remove the lingering outline after selecting the file.\n fileInput.addEventListener('click', () => {\n fileInput.blur()\n })\n fileInputContainer.append(fileInput, fileInputLabel)\n\n // Creating the final row element to append to the modal body.\n dropContainer.append(imageContainer, dropMessage, orMessage, fileInputContainer)\n fileSelectionRow.appendChild(dropContainer)\n\n // Adding functionality to the drag and drop file input.\n dropContainer.addEventListener('drop', selectDroppedFile.bind(this))\n dropContainer.addEventListener('drop', unhighlightDropArea)\n // Prevent file from being opened by the browser.\n dropContainer.ondragover = highlightDropArea\n dropContainer.ondragenter = highlightDropArea\n dropContainer.ondragleave = unhighlightDropArea\n\n fileInput.addEventListener('change', handleFileChange)\n\n const fileNamePlaceholder = document.createElement('center')\n fileNamePlaceholder.id = 'file-name-placeholder'\n fileNamePlaceholder.className = 'row'\n fileNamePlaceholder.innerText = ''\n fileSelectionRow.appendChild(fileNamePlaceholder)\n fileNamePlaceholder.classList.add('hidden')\n\n // Check if the continue button is invalid on the keystore upload modal and\n // click it if it should no longer be invalid.\n this.continueButton.addEventListener('invalid', () => {\n const fileFormElements = this.container.querySelectorAll('.dcp-modal-body input[type=\"file\"], .dcp-modal-body input[type=\"text\"]')\n const filledInFileFormElements = Array.from(fileFormElements).filter(fileFormElement => fileFormElement.value !== '')\n if (fileFormElements.length !== 0 && filledInFileFormElements.length !== 0) {\n this.continueButton.setCustomValidity('')\n // Clicking instead of dispatching a submit event to ensure other form validation is used before submitting the form.\n this.continueButton.click()\n }\n })\n\n return [fileInput, fileSelectionRow]\n\n /**\n * Checks that the dropped items contain only a single keystore file.\n * If valid, sets the file input's value to the dropped file.\n * @param {DragEvent} event - Contains the files dropped.\n */\n function selectDroppedFile (event) {\n // Prevent file from being opened.\n event.preventDefault()\n\n // Check if only one file was dropped.\n const wasOneFileDropped = event.dataTransfer.items.length === 1 ||\n event.dataTransfer.files.length === 1\n updateFileSelectionStatus(wasOneFileDropped)\n if (!wasOneFileDropped) {\n fileInput.setCustomValidity('Only one file can be uploaded.')\n fileInput.reportValidity()\n return\n } else {\n fileInput.setCustomValidity('')\n }\n\n // Now to use the DataTransfer interface to access the file(s), setting\n // the value of the file input.\n const file = event.dataTransfer.files[0]\n\n if (checkFileExtension(file)) {\n fileInput.files = event.dataTransfer.files\n fileInput.dispatchEvent(new Event('change'))\n }\n }\n\n function handleFileChange () {\n if (checkFileExtension(this.files[0]) && this.files.length === 1) {\n fileNamePlaceholder.innerText = `Selected File: ${this.files[0].name}`\n updateFileSelectionStatus(true)\n // Invoke a callback if additional functionality is required.\n if (typeof fileInputProperties.callback === 'function') {\n fileInputProperties.callback(this.files[0])\n }\n }\n }\n\n /**\n * Checks if the file extension on the inputted file is correct.\n * @param {File} file - The file to check\n * @returns {boolean} True if the file extension is valid, false otherwise.\n */\n function checkFileExtension (file) {\n // If there's no restriction, return true.\n if (!fileInputProperties.extension) {\n return true\n }\n const fileExtension = file.name.split('.').pop()\n const isValidExtension = fileExtension === fileInputProperties.extension\n updateFileSelectionStatus(isValidExtension)\n if (!isValidExtension) {\n fileInput.setCustomValidity(`Only a .${fileInputProperties.extension} file can be uploaded.`)\n fileInput.reportValidity()\n fileNamePlaceholder.classList.add('hidden')\n } else {\n fileInput.setCustomValidity('')\n }\n return isValidExtension\n }\n\n /**\n * Updates the file input to reflect the validity of the current file\n * selection.\n * @param {boolean} isValidFileSelection - True if a single .keystore file\n * was selected. False otherwise.\n */\n function updateFileSelectionStatus (isValidFileSelection) {\n imageContainer.innerHTML = ''\n const statusImage = document.createElement('span')\n statusImage.classList.add('fas', isValidFileSelection ? 'fa-check' : 'fa-times')\n statusImage.style.color = isValidFileSelection ? 'green' : 'red'\n imageContainer.appendChild(statusImage)\n\n if (!isValidFileSelection) {\n fileInput.value = null\n fileNamePlaceholder.classList.add('hidden')\n } else {\n fileNamePlaceholder.classList.remove('hidden')\n }\n\n // If the modal contains a password field for a keystore file, change its\n // visibility.\n const walletPasswordInputContainer = document.querySelector('.dcp-modal-body input[type=\"password\"]').parentElement.parentElement\n if (walletPasswordInputContainer) {\n if (isValidFileSelection) {\n walletPasswordInputContainer.classList.remove('hidden')\n const walletPasswordInput = document.querySelector('.dcp-modal-body input[type=\"password\"]')\n walletPasswordInput.focus()\n } else {\n walletPasswordInputContainer.classList.add('hidden')\n }\n }\n }\n\n function highlightDropArea (event) {\n event.preventDefault()\n this.classList.add('highlight')\n }\n\n function unhighlightDropArea (event) {\n event.preventDefault()\n this.classList.remove('highlight')\n }\n }\n\n /**\n * Sets up a custom tooltip to pop up when the passwords do not match, but are\n * valid otherwise.\n */\n addFormValidationForPasswordConfirmation () {\n const [newPassword, confirmPassword] = document.querySelectorAll('.dcp-modal-body input[type=\"password\"]')\n if (!newPassword || !confirmPassword) {\n throw Error('New Password field and Confirm Password fields not present.')\n }\n\n newPassword.addEventListener('input', checkMatchingPasswords)\n confirmPassword.addEventListener('input', checkMatchingPasswords)\n\n function checkMatchingPasswords () {\n if (newPassword.value !== confirmPassword.value &&\n newPassword.validity.valid &&\n confirmPassword.validity.valid) {\n newPassword.setCustomValidity('Both passwords must match.')\n } else if (newPassword.value === confirmPassword.value ||\n newPassword.validity.tooShort ||\n newPassword.validity.patternMismatch ||\n newPassword.validity.valueMissing ||\n confirmPassword.validity.tooShort ||\n confirmPassword.validity.patternMismatch ||\n confirmPassword.validity.valueMissing) {\n // If the passwords fields match or have become invalidated some other\n // way again, reset the custom message.\n newPassword.setCustomValidity('')\n }\n }\n }\n\n updateInvalidEmailMessage() {\n const email = document.querySelector('.dcp-modal-body input[id=\"email\"')\n if (!email){\n throw Error(\"Email field not present\")\n }\n email.addEventListener('input', checkValidEmail);\n function checkValidEmail() {\n if (!email.validity.patternMismatch &&\n !email.validity.valueMissing) {\n email.setCustomValidity('')\n } else {\n email.setCustomValidity(\"Enter a valid email address.\")\n }\n\n }\n }\n\n /**\n * Adds message(s) to the modal's body.\n * @param {string} messages - The message(s) to add to the modal's body.\n * @returns Paragraph element(s) containing the message(s) added to the\n * modal's body.\n */\n addMessage (...messages) {\n const elements = []\n const body = this.container.querySelector('.dcp-modal-body')\n for (let i = 0; i < messages.length; i++) {\n const row = document.createElement('div')\n row.className = 'row'\n\n const paragraph = document.createElement('p')\n paragraph.innerHTML = messages[i]\n paragraph.classList.add('message')\n row.appendChild(paragraph)\n body.appendChild(row)\n\n elements.push(paragraph)\n }\n\n if (elements.length === 1) return elements[0]\n else return elements\n }\n\n addHorizontalRule () {\n const body = this.container.querySelector('.dcp-modal-body')\n body.appendChild(document.createElement('hr'))\n }\n\n // Does what it says. Still ill advised to use unless you have to.\n addCustomHTML (htmlStr, browseCallback) {\n const elements = []\n const body = this.container.querySelector('.dcp-modal-body')\n body.innerHTML += htmlStr\n body.querySelector('#browse-button').addEventListener('click', browseCallback.bind(this, this))\n\n if (elements.length === 1) return elements[0]\n else return elements\n }\n\n addButton (...buttons) {\n const elements = []\n const body = this.container.querySelector('.dcp-modal-body')\n for (let i = 0; i < buttons.length; i++) {\n const row = document.createElement('div')\n row.className = 'row'\n\n let col = document.createElement('div')\n col.className = 'col-md-4'\n\n const description = document.createElement('span')\n description.innerText = buttons[i].description\n\n col.appendChild(description)\n row.appendChild(col)\n\n col = document.createElement('div')\n col.className = 'col-md-8'\n\n const button = document.createElement('button')\n button.innerText = buttons[i].label\n button.addEventListener('click', buttons[i].callback.bind(this, this))\n\n elements.push(button)\n\n col.appendChild(button)\n row.appendChild(col)\n\n body.appendChild(row)\n }\n\n if (elements.length === 1) return elements[0]\n else return elements\n }\n}\n\nexports.Modal = Modal;\n\n// Inject our special stylesheet from dcp-client only if we're on the portal webpage.\nif (typeof window !== 'undefined' && typeof document !== 'undefined' && dcpConfig.portal.location.hostname === window.location.hostname) {\n // <link rel='stylesheet' href='/css/dashboard.css'>\n const stylesheet = document.createElement('link')\n stylesheet.rel = 'stylesheet'\n // Needed for the duplicate check done later.\n stylesheet.id = 'dcp-modal-styles'\n\n const dcpClientBundle = document.getElementById('_dcp_client_bundle')\n let src\n if (dcpClientBundle) {\n src = dcpClientBundle.src.replace('dcp-client-bundle.js', 'dcp-modal-style.css')\n } else {\n src = dcpConfig.portal.location.href + 'dcp-client/dist/dcp-modal-style.css'\n }\n\n stylesheet.href = src\n // If the style was injected before, don't inject it again.\n // Could occur when loading a file that imports Modal.js and loading\n // comput.min.js in the same HTML file.\n if (document.getElementById(stylesheet.id) === null) {\n document.getElementsByTagName('head')[0].appendChild(stylesheet)\n }\n\n if (typeof {\"version\":\"e4988ba0b1c993e062fc7c536188bda3ea4de883\",\"branch\":\"release\",\"dcpClient\":{\"version\":\"4.1.12\",\"from\":\"git+ssh://git@gitlab.com/Distributed-Compute-Protocol/dcp-client.git#release\",\"resolved\":\"git+ssh://git@gitlab.com/Distributed-Compute-Protocol/dcp-client.git#169b90c5603b765cbd334308bb8ac57eddb28378\"},\"built\":\"Fri Oct 22 2021 16:01:33 GMT-0400 (Eastern Daylight Time)\",\"config\":{\"generated\":\"Fri 22 Oct 2021 04:01:30 PM EDT by erose on dione\",\"build\":\"debug\"},\"webpack\":\"4.46.0\",\"node\":\"v12.22.6\"} !== 'undefined' && typeof window.Modal === 'undefined') {\n window.Modal = Modal\n }\n}\n\n\n//# sourceURL=webpack:///./src/protocol-v3/modal.js?");
5474
5474
 
5475
5475
  /***/ }),
5476
5476
 
@@ -5504,7 +5504,7 @@ eval("/**\n * @file protocol/connection/batch.js\n * @author Ryan Ro
5504
5504
  /***/ (function(module, exports, __webpack_require__) {
5505
5505
 
5506
5506
  "use strict";
5507
- eval("/**\n * @file protocol/connection/connection.js\n * @author Ryan Rossiter\n * @author KC Erb\n * @date January 2020, Feb 2021\n *\n * A Connection object represents a connection to another DCP entity. \n * A DCP connection may 'live' longer than the underlying protocol's connection,\n * and the underlying protocol connection (or, indeed, protocol) may change\n * throughout the life of the DCP connection.\n * \n * DCP connections are uniquely identified by the DCP Session ID, specified by\n * the dcpsid property, present in every message body. This session id negotiated during connection,\n * with the initiator and target each providing half of the string.\n *\n * \n * State Transition Diagram for Connection.state:\n *\n * initial connecting established waiting close-wait closing closed\n * ====================================================================================================\n * |- connect() ->\n * |----- connect() ----->\n * |- establishTarget() ->\n * <-- revive() --|\n * |--------- doClose() ------->\n * |- doClose() ->\n * |--------------|---------------------|-------------> failTransport()\n * |--------------|---------------------|--------------|------------|------------> <------------| doClose()\n *\n * failTransport() takes a state from anywhere, sets it to waiting,\n * and sends it back to where it came from. doclose() takes a state\n * from anywhere and sends it to the coClose() state.\n *\n * Not until the established state can we count on things like a dcpsid, \n * peerAddress, identityPromise resolution and so on.\n */\n\n\n\nconst debugging = __webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope('dcp');\nconst debuggingScheduler = __webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope('scheduler');\nconst { EventEmitter } = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\");\nconst wallet = __webpack_require__(/*! dcp/dcp-client/wallet */ \"./src/dcp-client/wallet/index.js\");\nconst { DcpURL } = __webpack_require__(/*! dcp/common/dcp-url */ \"./src/common/dcp-url.js\");\nconst { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\nconst { assert } = __webpack_require__(/*! dcp/common/dcp-assert */ \"./src/common/dcp-assert.js\");\nconst { leafMerge } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\nconst kvin = __webpack_require__(/*! kvin */ \"./node_modules/kvin/kvin.js\");\nconst { Synchronizer } = __webpack_require__(/*! dcp/common/concurrency */ \"./src/common/concurrency.js\");\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\n\nconst perf = typeof performance === 'undefined'\n ? requireNative('perf_hooks').performance\n : performance;\n\nconst { Transport } = __webpack_require__(/*! ../transport */ \"./src/protocol-v4/transport/index.js\");\nconst { Sender } = __webpack_require__(/*! ./sender */ \"./src/protocol-v4/connection/sender.js\");\nconst { Receiver } = __webpack_require__(/*! ./receiver */ \"./src/protocol-v4/connection/receiver.js\");\nconst { MessageFactory } = __webpack_require__(/*! ./message-factory */ \"./src/protocol-v4/connection/message-factory.js\");\nconst { MessageLedger } = __webpack_require__(/*! ./message-ledger */ \"./src/protocol-v4/connection/message-ledger.js\");\nconst { ValidityStampCache } = __webpack_require__(/*! ./validity-stamp-cache */ \"./src/protocol-v4/connection/validity-stamp-cache.js\");\nconst { getGlobalIdentityCache } = __webpack_require__(/*! ./identity-cache */ \"./src/protocol-v4/connection/identity-cache.js\");\nconst { makeEBOIterator } = __webpack_require__(/*! dcp/common/dcp-timers */ \"./src/common/dcp-timers.js\");\n\nconst { ConnectionMessage } = __webpack_require__(/*! ./message */ \"./src/protocol-v4/connection/message.js\");\nconst { ConnectionRequest } = __webpack_require__(/*! ./request */ \"./src/protocol-v4/connection/request.js\");\nconst { ConnectionResponse } = __webpack_require__(/*! ./response */ \"./src/protocol-v4/connection/response.js\");\nconst { ConnectionBatch } = __webpack_require__(/*! ./batch */ \"./src/protocol-v4/connection/batch.js\");\nconst { ConnectionAck } = __webpack_require__(/*! ./ack */ \"./src/protocol-v4/connection/ack.js\");\nconst { ErrorPayloadCtorFactory } = __webpack_require__(/*! ./error-payload */ \"./src/protocol-v4/connection/error-payload.js\");\nconst isDebugBuild = __webpack_require__(/*! dcp/common/dcp-build */ \"./src/common/dcp-build.js\").build === 'debug';\n\nlet globalConnectionId = 0;\n\nconst CONNECTION_STATES = [\n 'initial',\n 'connecting',\n 'established',\n 'waiting', /* Targets only, when send fails */\n 'close-wait', /* Target of close message is in this state until response is acknowledged */\n 'closing',\n 'closed',\n]\n\nclass Connection extends EventEmitter {\n static get VERSION() {\n return '5.0.0'; // Semver format\n }\n\n static get VERSION_COMPATIBILITY() {\n return '5.0.0'; // Semver format, can be a range\n }\n\n /**\n * @constructor Connection form 4:\n * Create a DCP Connection object for an initiator.\n * @param {string} target The string version (ie href) of the URL of the target to connect to.\n * @param {wallet.IdKeystore} [idKeystore]\n * @param {Object} [connectionOptions]\n * @see form 1\n */\n /**\n * @constructor Connection form 3:\n * Create a DCP Connection object for an initiator.\n * @param {DcpURL|URL} target The URL of the target to connect to.\n * @param {wallet.IdKeystore} [idKeystore]\n * @param {Object} [connectionOptions]\n * @see form 1\n */\n /**\n * @constructor Connection form 2:\n * Create a DCP Connection object for a target.\n * @param {wallet.IdKeystore} [idKeystore]\n * @param {Object} [connectionOptions]\n * @see form 1\n */\n /**\n * @constructor Connection form 1\n * Create a DCP Connection object. \n * \n * @note Connection objects exist for the lifetime of a given DCP connection \n * (session), whether or not the underlying transport (eg internet protocol) is connected or not. Once \n * the DCP session has ended, this object has no purpose and is not reusable.\n * \n * @param {object|undefined} target Undefined when we are the target, or an object describing the target. This object \n * may contain the following properties; 'location' is mandatory:\n * - location: a DcpURL that is valid from the Internet\n * - friendLocation: a DcpURL that is valid from an intranet; if\n * both location and friendLocation specified, the best\n * one will be chosen by examining IP addresses\n * - identity: a object with an address property which is an\n * instanceof wallet.Address which corresponds to the peer's\n * identity; this overrides the identity cache unless\n * connectionOptions.strict is truey.\n * @param {wallet.IdKeystore} [idKeystore] The keystore used to sign messages; used for non-repudiation.\n * If not specified, a dynamically-generated keystore will be used.\n * \n * @param {Object} [connectionOptions] Extra connection options that aren't defined via dcpConfig.dcp.connectionOptions.\n * These options include:\n * - identityUnlockTimeout: Number of (floating-point) seconds to leave the identity \n * keystore unlocked between invocations of Connection.send\n */\n constructor(target, idKeystore, connectionOptions)\n {\n /* polymorphism strategy: rewrite all to form 1 before super */\n if (target instanceof wallet.Keystore) /* form 2 */\n { \n connectionOptions = arguments[2];\n idKeystore = arguments[1];\n target = undefined;\n }\n if (typeof connectionOptions === 'undefined')\n connectionOptions = {};\n\n if (target instanceof URL) /* form 3.2 */\n target = { location: new DcpURL(target) };\n else if (DcpURL.isURL(target)) /* form 3.1 */\n target = { location: new DcpURL(target) };\n else if (target instanceof String || typeof target === 'string') /* form 4 */\n target = { location: new DcpURL(target) };\n\n assert((typeof target === 'undefined') || (typeof target === 'object' && DcpURL.isURL(target.location)));\n assert(typeof connectionOptions === 'object');\n \n super(`Protocol Connection (${target ? 'initiator' : 'target'})`);\n\n if (target) {\n this.isInitiator = true;\n this.debugLabel = 'connection(i):';\n this._target = target;\n } else {\n this.isInitiator = false;\n this.debugLabel = 'connection(t):';\n }\n\n if (idKeystore) {\n this.identityPromise = (async () => await idKeystore)();\n } else {\n this.identityPromise = wallet.getId();\n }\n\n this.identityPromise.then((keystore) => {\n this.identity = keystore;\n debugging('connection') && console.debug(this.debugLabel, 'identity is', keystore.address);\n return keystore;\n });\n\n this.ttl = {\n ntp: false,\n }\n \n // Init internal state / vars\n this.state = new Synchronizer(CONNECTION_STATES[0], CONNECTION_STATES);\n this.state.on('change', (s) => this.emit('readyStateChange', s) );\n\n this._id = globalConnectionId++;\n this.debugLabel = this.debugLabel.replace(')', `#${this._id})`);\n debugging('connection') && console.debug(this.debugLabel, 'connection id is', this._id, `target is ${target && target.location}`);\n this.dcpsid = null;\n this.peerAddress = null;\n this.transport = null;\n this.maxConnectionTimeout = null;\n this.messageFactory = new MessageFactory(this);\n this.messageLedger = new MessageLedger(this);\n\n this.Message = ConnectionMessage(this);\n this.Request = ConnectionRequest(this.Message);\n this.Response = ConnectionResponse(this.Message);\n this.Batch = ConnectionBatch(this.Message);\n this.Ack = ConnectionAck(this.Message);\n this.ErrorPayload = ErrorPayloadCtorFactory(this);\n this.connectTime = Date.now();\n\n this.openRequests = {};\n ValidityStampCache.init();\n\n this.receiver = new Receiver(this, this.messageLedger);\n this.receiver.on('request', (req) => this.emit('request', req));\n\n debugging('connection') && console.debug(this.debugLabel, `new; ${target && target.location || '<target>'}`);\n\n this.configureConnectionForUrl = (url) => {\n if (typeof connectionOptions.ttl === 'number')\n connectionOptions = Object.assign({}, connectionOptions, { ttl: { default: connectionOptions.ttl } });\n \n this.url = url;\n this.connectionOptions = leafMerge(/* ordered from least to most specific */\n dcpConfig.dcp.connectionOptions.default,\n this.url && dcpConfig.dcp.connectionOptions[this.url.hostname],\n this.url && dcpConfig.dcp.connectionOptions[this.url.origin],\n dcpConfig.dcp.connectionOptions[this.isInitiator ? this.url.href : 'target'],\n connectionOptions\n );\n\n // ensure we're using a _copy_ of the transports list, not mutating the original\n this.connectionOptions.transports = [...this.connectionOptions.transports];\n this.originalConnectionTransports = [].concat(this.connectionOptions.transports);\n this.transportsTried = 0;\n\n this.ttl = leafMerge(this.ttl, this.connectionOptions.ttl);\n this.unlockTimeout = this.connectionOptions.identityUnlockTimeout;\n this.connectionOptions.id = this._id;\n this.EBOIterator = makeEBOIterator(1000, 60000);\n\n assert(this.unlockTimeout >= 0);\n assert(typeof this.connectionOptions.ttl.min === 'number');\n assert(typeof this.connectionOptions.ttl.max === 'number');\n assert(typeof this.connectionOptions.ttl.default === 'number');\n\n this.secureLocation = determineIfSecureLocation(this);\n }\n }\n\n /**\n * This method is an instantiator/factory function for building a connection\n * that will act as the target in a new protocol connection. It's a little\n * like making a new connection and calling `connect` on it, except that\n * instead of having a url to connect to we have a transport which should\n * be ready to emit the connect message from the initiator.\n * \n * @param {wallet.Keystore} ks - Keystore to associate to the new connection.\n */\n static async newTarget(url, ks, transport) {\n const target = new Connection(undefined, ks, {\n ttl: {\n ntp: true, // targets should always be NTP-synced\n }\n });\n\n target.configureConnectionForUrl(url);\n assert(target.connectionOptions.ttl.ntp);\n \n await target.identityPromise;\n target.transport = transport;\n target.sender = new Sender(target);\n target.state.set('initial', 'connecting');\n return target;\n }\n\n /**\n * When invoked by the initator, this method establishes the connection by connecting\n * to the target provided to the constructor.\n * \n * When a dcpConfig-fragment is used as the target object, we determine automatically if \n * we should be using the location or the friendLocation property at this point.\n *\n * This function is also where the connectionOptions are merged together. This means \n * that if either of these objects come from dcpConfig and we HUP a service running this\n * code, that the new configuration will be used to connect, instead of the configuration \n * that was defined at the time of construction.\n *\n * @throws when called by a target, or when called more than once\n */\n async connect() {\n var presharedPeerAddress;\n \n let transportName;\n \n if (!this.state.in(['initial', 'connecting'])) {\n throw new Error(`Can only invoke connect in initial/connecting state. Current state: ${this.state.valueOf()}.`, );\n }\n\n // make connect re-entrant if called during the connecting phase\n if (this.state.is('connecting'))\n {\n /* nth connect attempt on same Connection instance before actual connection -- swallow attempts and wait until connected */\n do\n {\n await this.state.until(['established', 'close-wait', 'closing', 'closed']);\n } while(this.state.is('waiting'));\n\n if (!this.state.is('established'))\n throw new Error(`Could not establish connection to ${this.url}; in state ${this.state}`);\n return;\n }\n\n this.state.set('initial', 'connecting');\n\n if (this._target && this._target.hasOwnProperty('friendLocation') && await isFriendlyURL(this._target.friendLocation))\n this.configureConnectionForUrl(this._target.friendLocation);\n else\n this.configureConnectionForUrl(this._target.location);\n\n const transport = this.fetchTransport();\n this.transport = transport.transport;\n transportName = transport.moduleName;\n this.sender = new Sender(this);\n // create sender before promises so that we can still enqueue messages before hopping off the event loop\n await this.identityPromise;\n await this.connectToTarget(transportName);\n const establishResults = await this.sender.establish().catch( (err) => {\n console.error('Failed to establish connection.');\n this.close(err, true);\n throw err;\n });\n const dcpsid = establishResults.dcpsid;\n const peerAddress = wallet.Address(establishResults.peerAddress);\n\n if (!this.connectionOptions.strict && this._target.identity)\n {\n if (determineIfSecureConfig())\n {\n presharedPeerAddress = (await this._target.identity).address;\n debugging('connection') && console.debug('Using preshared peer address', presharedPeerAddress);\n }\n }\n this.ensureIdentity(peerAddress, presharedPeerAddress); /** XXXwg possible resource leak: need cleanup; need try {} catch->emit(cleanup) */\n \n // checks have passed, now we can set props\n this.peerAddress = peerAddress;\n if (this.dcpsid)\n throw new DCPError(`Reached impossible state in connection.js; dcpsid already specified ${this.dcpsid} (${this.url})`, 'DCPC-1004');\n this.dcpsid = dcpsid;\n \n // Update state\n this.state.set('connecting', 'established'); /* established => dcpsid has been set */\n this.emit('connected', this.url);\n this.sender.finishSetup();\n }\n\n moreTransportsExist() {\n return this.connectionOptions\n && this.connectionOptions.transports\n && Array.isArray(this.connectionOptions.transports)\n && this.connectionOptions.transports.length > 0;\n }\n \n /**\n * Initiators only.\n * Try to get and establish a transport from the transports list.\n * @returns {Transport} a new transport instance or false if no transports are left to try\n */\n fetchTransport() {\n //We want to cycle our possible transports until the maxConnectionTimeout runs out or we have a connection\n const moduleName = this.connectionOptions.transports.shift();\n this.connectionOptions.transports.push(moduleName);\n this.transportsTried++;\n debugging() && console.log(this.debugLabel, 'fetchTransport trying ', moduleName, `remaining transports: ${this.connectionOptions.transports.length}`)\n if (!moduleName) {\n console.error('Error: All transports have failed to connect/transmit within limits. Closing connection.');\n this.close('all transports failed', true).catch(error => {\n console.error(`Error: failed to close transport:`, error);\n });\n throw new DCPError(`Failed to connect to ${this.url}, no transport could be established`, 'ENOTRANSPORT');\n }\n const TransportClass = Transport.require(moduleName);\n const transport = new TransportClass(null, this.connectionOptions);\n transport.on('message', (m) => {\n this.onMessage(JSON.parse(m))\n });\n return { transport, moduleName };\n }\n\n /**\n * Initiators only\n * Resolves when we have connected to target using a transport.\n * Calls \"failTransport\" if we run out of time / attempts to connect.\n * Let's transport handle failure but also is watching for it too.\n */\n async connectToTarget(currentTransport) {\n debugging('connection') && console.log(this.debugLabel, `Using transport \"${this.transport.name}\" ... connecting to ${this.url}.`);\n const globalWaitMs = dcpConfig.dcp.maxConnectionTimeout;\n const connectWaitMs = this.transport.connect(this.url, this.connectionOptions);\n\n this.maxConnectionTimeout = this.maxConnectionTimeout ? this.maxConnectionTimeout\n : setTimeout(this.close.bind(this, `Timeout to have a transport connect reached at ${globalWaitMs} ms`, true), globalWaitMs);\n\n await new Promise( (resolve) => {\n // Listener for a connect-failed, will invoke EBO algorithm to retry the connection\n this.transport.once('connect-failed', () => {\n if (this.transportsTried >= this.connectionOptions.transports.length)\n {\n this.transportsTried = 0;\n const backoffTime = this.EBOIterator.next().value;\n debugging('connection') && console.debug(this.debugLabel, 'connecting in', backoffTime / 1000, 'seconds');\n this.failTransport('Connect-failed event handler fired.', backoffTime);\n }\n else\n {\n this.failTransport('Connect-failed event handler fired.');\n }\n });\n\n this.transport.once('connected', () => {\n clearTimeout(this.maxConnectionTimeout);\n this.maxConnectionTimeout = null;\n /* When we successfully connect a transport, we know that transport has potential to consistently work,\n * and disconnects may happen for unrelated reason (such as switching wifi causing a disconnect). \n * Thus, we recreate our transport options list with this transport at the top\n */\n this.connectionOptions.transport = [currentTransport].concat(this.originalConnectionTransports);\n resolve();\n });\n });\n this.transport.on('reconnect', () => {\n debugging('connection') && console.log(this.debugLabel, `Transport reconnected to ${this.url}`);\n // Send a keepalive to the remote end, to establish the new Transport with this Connection\n this.keepalive();\n this.emit('reconnect');\n });\n }\n\n /**\n * Target gains full status once dcpsid and peerAddress\n * are provided by first connect request.\n * @param {string} dcpsid dcpsid\n * @param {wallet.Address} peerAddress Address of peer\n */\n establishTarget(dcpsid, peerAddress) {\n this.connectResponseId = Symbol(); // un-register ConnectResponse\n this.peerAddress = peerAddress;\n if (this.dcpsid)\n throw new DCPError(`Reached impossible state in connection.js; dcpsid already specified ${this.dcpsid}!=${dcpsid} (${this.url})`, 'DCPC-1005');\n this.dcpsid = dcpsid;\n this.state.set('connecting', 'established'); /* established => dcpsid has been set */\n }\n\n ensureIdentity (peerAddress, presharedPeerAddress) {\n let identityCache = getGlobalIdentityCache();\n \n if (!identityCache.validateIdentity(this.url.href, peerAddress)) {\n if (presharedPeerAddress)\n {\n console.error(`**** Warning: preshared peer address ${presharedPeerAddress} does not match address ${identityCache.getIdentity(this.url.href)} provided for ${this.url} in identity cache ****`);\n __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\").sleep(10);\n identityCache.insert(this.url.href, peerAddress);\n }\n throw new DCPError(`Identity address ${peerAddress} does not match the saved key for ${this.url}`,'EADDRCHANGE');\n }\n }\n\n /**\n * Check that the transport has given us a message worth dealing with then\n * either let the receiver handle it (message) or the message ledger (ack).\n * @param {object} message parsed (but not validated) message object\n */\n async onMessage (message) {\n if (this.state.is('closed')) {\n debugging('connection') && console.warn(this.debugLabel, 'onMessage was called on a closed connection.');\n return;\n }\n /**\n * We always ack a duplicate transmission.\n * This must happen before validation since during startup we may lack a\n * nonce or dcpsid (depending on whether initiator or target + race).\n */\n if (this.isDuplicateTransmission(message)) {\n debugging('connection') && console.debug(this.debugLabel, 'duplicate message:', message.body);\n this.transport.send(this.lastAckSigned);\n return;\n }\n\n let messageIsValid = false;\n try {\n messageIsValid = this.isMessageValid(message);\n } catch (error) {\n console.error('Message passed to validator threw an Error:', error);\n }\n /* messageIsValid with either be true, or a string with the reason the message isn't valid*/\n if (messageIsValid !== true) {\n if (debugging('connection'))\n this.close(`invalid message: ${messageIsValid}\\n ${JSON.stringify(message)}`, true);\n else\n this.close(`invalid message: ${messageIsValid}`, true);\n return;\n }\n\n if (message.body.type === \"ack\") {\n const ack = new this.Ack(message.body);\n this.messageLedger.handleAck(ack);\n return;\n } else if (message.body.type !== 'unhandled-message') {\n this.lastMessage = message;\n await this.ackMessage(message);\n }\n this.receiver.onMessage(message);\n }\n\n async ackMessage(message) {\n debugging('connection') && console.debug(this.debugLabel, 'acking message.');\n const ack = new this.Ack(message);\n const signedMessage = await ack.sign(this.identity);\n this.transport.send(signedMessage);\n this.lastAck = ack;\n this.lastAckSigned = signedMessage;\n }\n\n /**\n * Checks if the batch we just received has the same nonce\n * as the most-recently received batch.\n * @param {object} messageJSON\n */\n isDuplicateTransmission(messageJSON) {\n return this.lastMessage && this.lastMessage.body.nonce === messageJSON.body.nonce;\n }\n\n /**\n * This method is used to perform validation on all types of messages.\n * It validates the signature, DCPSID, nonce, and the peerAddress.\n * @param {Object} message\n */\n isMessageValid(message) {\n const owner = new wallet.Address(message.owner);\n const signatureValid = owner.verifySignature(message.body, message.signature);\n // keeps track of the connection url so that if the message is invalid, the source can be traced easily\n // if message is invalid, it will return the reason for the error plus the URL, otherwise will return true.\n let currURL = \"Could not retrieve connection URL\"\n if (this.url) {\n currURL = this.url.href;\n }\n if (!signatureValid) {\n debugging('connection') && console.warn(\"Message has an invalid signature, aborting connection\");\n return \"invalid message signature \" + currURL;\n } else if (message.body.type === 'unhandled-message') {\n // this special message type may not have a dcpsid, peerAddress, etc.\n debugging('connection') && console.warn(\"Target Error - target could not process message.\", JSON.stringify(message.body),\n \"Aborting connection.\");\n return \"target could not process message \" + currURL;\n } else if (this.state.in(['established', 'closing', 'close-wait'])) {\n const body = message.body;\n\n /**\n * Security note:\n * We don't require the dcpsid to match on an ack because the connect response\n * ack doesn't have a dcpsid until after it is processed. Also ack's are protected\n * by ack tokens and signatures, so this doesn't leave a hole, just an inconsistency.\n */\n if (message.body.type !== 'ack' && body.dcpsid !== this.dcpsid) {\n debugging('connection') && console.warn(\"Received message's DCPSID does not match, aborting connection\\n\",\n \"Wallet owner:\", JSON.stringify(owner), '\\n',\n \"(ours)\", this.dcpsid, (Date.now() - this.connectTime)/1000, \"seconds after connecting - state:\", this.state._, \"\\n\", \n \"(theirs)\", body.dcpsid);\n if(body.dcpsid.substring(0, body.dcpsid.length/2) !== this.dcpsid.substring(0, this.dcpsid.length/2)){\n debugging('connection') && console.warn(\" Left half of both DCPSID is different\");\n }\n if(body.dcpsid.substring(body.dcpsid.length/2 + 1, body.dcpsid.length) !== this.dcpsid.substring(this.dcpsid.length/2 + 1, body.dcpsid.length)){\n debugging('connection') && console.warn(\" Right half of both DCPSID is different\");\n }\n debugging('connection') && console.log(\"The full message that caused this error:\");\n debugging('connection') && console.log(JSON.stringify(message, null, 2));\n debugging('connection') && console.log(\"The stack up to this point is:\");\n debugging('connection') && console.trace();\n debugging('connection') && console.log(\"The target has address:\")\n debugging('connection') && console.log(JSON.stringify(this.peerAddress));\n debugging('connection') && console.log(\"The transport is:\");\n debugging('connection') && console.log(this.transport);\n return \"DCPSID do not match\";\n } else if (!this.peerAddress.eq(owner)) {\n debugging('connection') && console.warn(\"Received message's signature address does not match peer address, aborting connection\\n\",\n \"(signature addr)\", owner, '\\n',\n \"(peer addr)\", this.peerAddress);\n return \"received message signature does not match peer address\";\n } else if (message.body.type !== 'ack' && this.lastAck.nonce !== message.body.nonce) {\n debugging('connection') && console.warn(\"Received message's nonce does not match expected nonce, aborting connection\\n\");\n debugging('connection') && console.debug(this.debugLabel, this.lastAck.nonce, message.body.nonce);\n return \"received message's nonce does not match expected nonce \" + currURL;\n }\n } else {\n // only validate signature for unestablished connections\n // while connecting, the target gets its nonce from the initiator\n assert(this.state.is('connecting'));\n if (!this.isInitiator) this.sender.nonce = message.body.nonce;\n }\n\n return true;\n }\n\n /**\n * Targets Only.\n * The receiver creates a special connect response and the connection\n * needs to know about it to get ready for the ack. See `isWaitingForAck`.\n * @param {Message} message message we are sending out and waiting to\n * ack'd, probably a batch containing the response.\n */\n registerConnectResponse(message) {\n this.connectResponseId = message.id;\n }\n\n /**\n * Targets only\n * During the connection process a target sends a connect\n * response to an initiator and the initiator will ack it. Since transports\n * are not tightly coupled, we have no authoritative way to route the ack back\n * to the right connection. So a connection briefly registers the ack it\n * is looking for in this case. It will formally validate the ack after routing.\n * @param {string} messageId id of the message this ack is acknowledging.\n */\n isWaitingForAck(messageId) {\n return messageId === this.connectResponseId;\n }\n\n /**\n * Targets: nothing to do, we are waiting for initiator.\n * Initiators: give up on current transport and try next available option.\n * For now we just have socketio so that means we throw an error.\n * @return {Promise<boolean>} indicating whether we were able to find a new transport.\n */\n async failTransport (failureDescription, retryWaitTime = 0) {\n let transportName;\n const preWaitingState = this.state.valueOf(); /* XXXwg bogus - need explicit and reasonable source states */\n\n /* If we are already in process of closing or already waiting, we don't want to beat \n * the dead horse. Let it stay dead with no transport\n */\n if (['closing', 'closed', 'close-wait', 'waiting'].includes(preWaitingState))\n return;\n\n this.state.testAndSet(preWaitingState, 'waiting');\n debugging('connection') && console.log(this.debugLabel, `Transport \"${this.transport.name}\" has failed.`);\n \n if (this.isInitiator) {\n if (!this.moreTransportsExist()) {\n console.error('Transport failed:', failureDescription);\n // fetchTransport will immediately fail\n }\n debugging('connection') && console.log(this.debugLabel, `Attempting to use next transport.`);\n this.transport.close();\n const transport = this.fetchTransport();\n this.transport = transport.transport;\n transportName = transport.moduleName;\n \n await new Promise(r => setTimeout(r, retryWaitTime));\n\n this.state.set('waiting', preWaitingState);\n await this.connectToTarget(transportName);\n }\n }\n\n /**\n * Targets only\n * Give target another transport to try sending messages on again.\n */\n revive(transport) {\n this.transport = transport;\n this.state.set('waiting', 'established');\n }\n\n /**\n * Put connection into close-wait state so that a call to `close`\n * in this state will *not* trigger sending a `close` message to the peer.\n * Then call close once the passed promise resolves.\n */\n async closeWait (waitPromise) {\n const preCloseState = this.state.valueOf();\n this.state.set(preCloseState, 'close-wait');\n debugging('connection') && console.debug(this.debugLabel, `responding to close. state=closeWait dcpsid=${this.dcpsid}`);\n // wait for waitPromise to resolve, reject if not within time limit\n try {\n await new Promise((resolve, reject) => {\n const closeWaitTimeout = setTimeout(() => {\n reject(new Error('Connection.closeWait timed out'));\n }, 10000); /* hardcode 10s wait /wg mar 2021 */\n waitPromise.then(resolve, reject).finally(() => clearTimeout(closeWaitTimeout));\n })\n debugging('connection') && console.debug(this.debugLabel, `close response ACK'd; closing. dcpsid=${this.dcpsid}`);\n } catch (error) {\n debugging('connection') && console.debug(this.debugLabel, `${error.message}; closing. dcpsid=${this.dcpsid}`);\n }\n // continue with close in either case\n let reason = 'received close from peer';\n if (this.url)\n reason += ` (${this.url})`;\n this.doClose(preCloseState, reason, true);\n await new Promise( r => this.once('close', r));\n }\n\n /**\n * This method will begin gracefully closing the protocol connection.\n * It will only close after sending all pending messages.\n * \n * @param {string|Error} [reason] Either an Error or a message to use in the Error that will reject pending sends.\n * @param {boolean} [immediate] If true, does not wait to send messages or the `close` request.\n *\n * @resolve when the connection has been confirmed closed and the close event has been emitted.\n */\n async close (reason=null, immediate=false) {\n if (this.state.is('closed')) return;\n\n // Use event to resolve this function call. This guarantees that a closed connection\n // does not emit an event.\n const onClosePromise = new Promise((resolve) => {\n this.once('end', resolve)\n });\n\n const preCloseState = this.state.valueOf();\n debugging('connection') && \n console.debug(this.debugLabel, \n `close; dcpsid=${this.dcpsid} state=${preCloseState} reason=${reason} immediate=${immediate} stack=${new Error().stack}`);\n\n // If we're already closing, wait for it to complete then resolve\n if (this.state.in(['close-wait', 'closing'])) {\n await onClosePromise;\n return;\n }\n\n // Put in closing state no matter the current state\n this.state.set(preCloseState, 'closing');\n\n // Perform actual work of closing\n this.doClose(preCloseState, reason, immediate);\n // wait for the close event to resolve the promise.\n await onClosePromise;\n }\n\n /**\n * sends close message to peer and waits for response\n */\n async closeGracefully() {\n await new Promise((resolve, reject) => {\n // reject if we don't resolve within time limit\n const timeoutTimer = setTimeout(() => {\n reject(new DCPError('Connection.close timed out', 'DCPC-1003'));\n }, this.connectionOptions.closeTimeout * 1000);\n \n if (this.transport)\n {\n /* If we got as far as initializing a transport during connect(), send close\n * message to peer, should get a response before time is up.\n */\n const closeMessage = this.messageFactory.buildMessage('close');\n this.sender.enqueue(closeMessage).then(resolve, reject).finally(() => clearTimeout(timeoutTimer));\n }\n })\n // hop off event loop so that close response ack can get out\n await new Promise(r=>setTimeout(r));\n }\n\n /**\n * Core close functionality shared by `close` and `closeWait`\n */\n async doClose(preCloseState, reason, immediate) {\n // Stash dcpsid\n const dcpsid = this.dcpsid;\n \n // Emit the close event the moment we know we are going to close, \n // so we can catch the close event and reopen the connection\n if (preCloseState === 'initial')\n {\n /**\n * Emitting this event so that the awaited `onClosePromise` in the `close`\n * method does not hang unexpectedly.\n */\n this.emit('close');\n return;\n }\n else\n {\n this.emit('close', dcpsid);\n }\n\n if (preCloseState === 'established' && !immediate) {\n try {\n await this.closeGracefully();\n } catch(e) {\n if (e.code === 'DCPC-1003') {\n if (__webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\").platform == 'nodejs') {\n var dnsA = await requireNative('dns').promises.lookup(this.url.hostname, {family: 4 });\n console.warn(`Warning: could not close connection gracefully. connectionid=${this._id}, dcpsid=,${this.dcpsid}, url=${this.url ? this.url.href : 'unknown url'} - (${e.message})`);\n }\n else {\n console.warn(`Warning: could not close connection gracefully(${e.message})`);\n };\n }\n else\n throw e;\n }\n }\n\n // can delete these now that we've sent the close message\n this.dcpsid = null;\n this.peerAddress = null;\n\n\n // Reject any pending transmissions in the message ledger\n let rejectErr;\n if (reason instanceof Error) {\n rejectErr = reason;\n } else {\n let message;\n if (typeof reason === 'string' || reason instanceof String ) {\n message = reason;\n } else {\n if (this.url)\n message = `Connection closed (url: ${this.url}, dcpsid: ${dcpsid})`;\n else\n message = `Connection closed (dcpsid: ${dcpsid})`;\n }\n rejectErr = new DCPError(message, 'DCPC-1002');\n }\n \n // If we are closing the connection, we don't care about failed messages. Close it silently\n this.messageLedger.silentFailMessage = true;\n this.messageLedger.failAllTransmissions(rejectErr);\n if (this.transport)\n {\n this.sender.shutdown(); \n this.transport.close();\n }\n \n // manage internal state\n this.state.setIf('closing', 'closed');\n this.state.setIf('close-wait', 'closed');\n this.emit('end');\n }\n\n /**\n * Sends a message to the connected peer. If the connection has not yet been established,\n * this routine will first invoke this.connect().\n * \n * @param {...any} args\n * @returns {Promise<Response>} a promise which resolves to a response.\n */\n async send(...args) {\n if (this.state.in(['initial','connecting'])) {\n // Original message will be rejected so we don't want a second reject\n // here since it would be uncaught.\n await this.connect().catch( e => {\n throw new DCPError(`Connection (${this._id}) failed to connect to ${this.url ? this.url.href : '<unknown url>'}: ${e.message}`, e.code);\n });\n }\n\n if (this.state.in(['closing', 'closed'])) {\n let e = new DCPError(`Connection (${this._id}) in state '${this.state}' cannot send. (url: ${this.url})`, 'DCPC-1001');\n throw e;\n }\n\n const message = this.messageFactory.buildMessage(...args);\n return this.sender.enqueue(message);\n }\n\n /**\n * Sends a v3-over-v4 request\n * @param url {string} v3 DcpURL\n * @param message {object} v3 \"message\" value; may be empty\n * @param authKey {object} (v3 signing|v4 authorizing) keystore, if different from identity\n * @return {Promise<object>} Resolves with v3 route's response data\n */\n async sendv3(url, message = {}, authKey = null) {\n const v3Data = {\n url,\n message,\n };\n \n const req = new this.Request('v3', v3Data);\n\n if (authKey && !this.identity.address.eq(authKey.address)) {\n await req.authorize(authKey);\n }\n \n let response = await req.send()\n\n .catch((error) => {\n return {\n success: false,\n payload: error,\n }\n });\n\n // fail at v4-protocol-level? throw it\n if (!response.success) {\n throw response.payload;\n }\n \n // fail at v3-level? also throw it\n if (response.payload.v3status === 'reject')\n throw response.payload.v3rejection;\n \n return response.payload.v3resolution;\n }\n\n /**\n * This routine returns the current time for the purposes of\n * populating the Request message payload.validity.time property.\n * \n * @returns {Number} the integer number of seconds which have elapsed since the epoch\n */\n currentTime() {\n let msSinceEpoch;\n if (this.ttl.ntp) {\n msSinceEpoch = Date.now();\n } else {\n const msSinceLastReceipt = perf.now() - this.receiver.lastResponseTiming.receivedMs;\n msSinceEpoch = this.receiver.lastResponseTiming.time * 1000 + msSinceLastReceipt;\n }\n return Math.floor(msSinceEpoch / 1000);\n }\n\n /**\n * This method sends a keepalive to the peer, and resolves when the response has been received.\n */\n keepalive() {\n return this.send('keepalive');\n }\n\n /**\n * This method checks to see a service connection is established, typically will be given a timeOut condition when invoked\n */\n async setCheckConnectionTimeout(source) {\n // failTimeout is checked to account for a race condition if the connection is established at the same time as the fail timeout expires \n let failTimeout = false;\n function checkConnectionEstablished() {\n if(failTimeout){\n failTimeout = false;\n throw new DCPError(`Connection to ${this.url} from ${source} not established within 30 seconds, instead is in state ${this.state}`, 'DCP-1006');\n }\n }\n failTimeout = setTimeout(checkConnectionEstablished.bind(this), 30000);\n this.once('connected', () => {\n if(failTimeout){\n clearTimeout(failTimeout);\n failTimeout = false;\n }\n });\n await this.connect();\n }\n\n}\n\n/**\n * Returns true if friendLocation should work in place of location from this host.\n * This allows us to transparently configure inter-daemon communication that uses\n * local LAN IPs instead of bouncing off the firewall for NAT.\n */\nasync function isFriendlyURL(url)\n{\n var remoteIp, dnsA;\n var ifaces;\n \n if (url.hostname === 'localhost')\n return true;\n\n switch(url.protocol)\n {\n case 'http:':\n case 'https:':\n case 'ws:':\n case 'tcp:':\n case 'udp:':\n case 'dcpsaw:':\n break;\n default:\n return false;\n }\n\n /* Consider same-origin match friendly */\n if (__webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\").isBrowserPlatform)\n return url.origin === window.location.origin;\n\n /* Convert an IP address to a 32-bit int in network order */\n function i32(addr)\n {\n var ret = 0;\n var octets = addr.split('.');\n\n ret |= octets[0] << 24; /* Note: JS ints are signed 32, but that doesn't matter for masking */\n ret |= octets[1] << 16;\n ret |= octets[2] << 8;\n ret |= octets[3] << 0;\n\n return ret;\n }\n \n /* Consider machines in same IPv4 subnet friendly */\n dnsA = await requireNative('dns').promises.lookup(url.hostname, {family: 4});\n if (!dnsA)\n return false;\n remoteIp = i32(dnsA.address);\n ifaces = requireNative('os').networkInterfaces();\n for (let ifaceName of Object.keys(ifaces))\n {\n for (let alias of ifaces[ifaceName])\n {\n if (alias.family != 'IPv4')\n continue;\n\n let i32_addr = i32(alias.address);\n let i32_mask = i32(alias.netmask);\n\n if ((i32_addr & i32_mask) === (remoteIp & i32_mask))\n return true;\n }\n }\n\n return false;\n}\n\n/** \n * Determine if we got the scheduler config from a secure source, eg https or local disk.\n * We assume that all https transactions have PKI-CA verified.\n *\n * @note protocol::getSchedulerConfigLocation() is populated via node-libs/config.js or dcp-client/index.js\n *\n * @returns true or falsey\n */\nfunction determineIfSecureConfig()\n{\n var schedulerConfigLocation = __webpack_require__(/*! dcp/protocol-v4 */ \"./src/protocol-v4/index.js\").getSchedulerConfigLocation();\n var schedulerConfigSecure;\n \n if (schedulerConfigLocation && (schedulerConfigLocation.protocol === 'https:' || schedulerConfigLocation.protocol === 'file:'))\n {\n debugging('strict-mode') && console.log(`scheduler config location ${schedulerConfigLocation} is secure`); /* from casual eavesdropping */\n schedulerConfigSecure = true;\n }\n\n if (isDebugBuild)\n {\n debugging('strict-mode') && console.log(`scheduler config location is always secure for debug builds`);\n schedulerConfigSecure = 'debug';\n }\n\n debuggingScheduler() && console.debug(`Config Location ${schedulerConfigLocation} is ${!schedulerConfigSecure ? 'not secure' : 'secure-' + schedulerConfigSecure}`);\n return schedulerConfigSecure;\n}\n\n/**\n * Determine if a URL is secure by examinining the protocol, connection, and information about the \n * process; in particular, we try to determine if the dcp config was securely provided, because if \n * it wasn't, then we can't have a secure location, since the origin could be compromised.\n * \n * \"Secure\" in this case means \"secure against casual eavesdropping\", and this information should only\n * be used to refuse to send secrets over the transport or similar.\n *\n * @returns true or falsey\n */\nfunction determineIfSecureLocation(conn)\n{\n var isSecureConfig = determineIfSecureConfig();\n var secureLocation;\n \n if (!isSecureConfig) /* can't have a secure location without a secure configuration */\n return null;\n \n if (isDebugBuild || conn.url.protocol === 'https:' || conn.url.protocol === 'tcps:')\n secureLocation = true;\n else if (conn.isInitiator && conn._target.hasOwnProperty('friendLocation') && conn.url === conn._target.friendLocation)\n secureLocation = true;\n else if (conn.connectionOptions.allowUnencryptedSecrets)\n secureLocation = 'override';\n else\n secureLocation = false;\n\n (debugging() || debuggingScheduler()) && console.debug(`Location ${conn.url} is ${!secureLocation ? 'not secure' : 'secure-' + secureLocation}`);\n \n return secureLocation;\n}\n\nexports.Connection = Connection;\n\n\n//# sourceURL=webpack:///./src/protocol-v4/connection/connection.js?");
5507
+ eval("/**\n * @file protocol/connection/connection.js\n * @author Ryan Rossiter\n * @author KC Erb\n * @date January 2020, Feb 2021\n *\n * A Connection object represents a connection to another DCP entity. \n * A DCP connection may 'live' longer than the underlying protocol's connection,\n * and the underlying protocol connection (or, indeed, protocol) may change\n * throughout the life of the DCP connection.\n * \n * DCP connections are uniquely identified by the DCP Session ID, specified by\n * the dcpsid property, present in every message body. This session id negotiated during connection,\n * with the initiator and target each providing half of the string.\n *\n * \n * State Transition Diagram for Connection.state:\n *\n * initial connecting established waiting close-wait closing closed\n * ====================================================================================================\n * |- connect() ->\n * |----- connect() ----->\n * |- establishTarget() ->\n * <-- revive() --|\n * |--------- doClose() ------->\n * |- doClose() ->\n * |--------------|---------------------|-------------> failTransport()\n * |--------------|---------------------|--------------|------------|------------> <------------| doClose()\n *\n * failTransport() takes a state from anywhere, sets it to waiting,\n * and sends it back to where it came from. doclose() takes a state\n * from anywhere and sends it to the coClose() state.\n *\n * Not until the established state can we count on things like a dcpsid, \n * peerAddress, identityPromise resolution and so on.\n */\n\n\n\nconst debugging = __webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope('dcp');\nconst debuggingScheduler = __webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope('scheduler');\nconst { EventEmitter } = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\");\nconst wallet = __webpack_require__(/*! dcp/dcp-client/wallet */ \"./src/dcp-client/wallet/index.js\");\nconst { DcpURL } = __webpack_require__(/*! dcp/common/dcp-url */ \"./src/common/dcp-url.js\");\nconst { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\nconst { assert } = __webpack_require__(/*! dcp/common/dcp-assert */ \"./src/common/dcp-assert.js\");\nconst { leafMerge } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\nconst kvin = __webpack_require__(/*! kvin */ \"./node_modules/kvin/kvin.js\");\nconst { Synchronizer } = __webpack_require__(/*! dcp/common/concurrency */ \"./src/common/concurrency.js\");\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\n\nconst perf = typeof performance === 'undefined'\n ? requireNative('perf_hooks').performance\n : performance;\n\nconst { Transport } = __webpack_require__(/*! ../transport */ \"./src/protocol-v4/transport/index.js\");\nconst { Sender } = __webpack_require__(/*! ./sender */ \"./src/protocol-v4/connection/sender.js\");\nconst { Receiver } = __webpack_require__(/*! ./receiver */ \"./src/protocol-v4/connection/receiver.js\");\nconst { MessageFactory } = __webpack_require__(/*! ./message-factory */ \"./src/protocol-v4/connection/message-factory.js\");\nconst { MessageLedger } = __webpack_require__(/*! ./message-ledger */ \"./src/protocol-v4/connection/message-ledger.js\");\nconst { ValidityStampCache } = __webpack_require__(/*! ./validity-stamp-cache */ \"./src/protocol-v4/connection/validity-stamp-cache.js\");\nconst { getGlobalIdentityCache } = __webpack_require__(/*! ./identity-cache */ \"./src/protocol-v4/connection/identity-cache.js\");\nconst { makeEBOIterator } = __webpack_require__(/*! dcp/common/dcp-timers */ \"./src/common/dcp-timers.js\");\n\nconst { ConnectionMessage } = __webpack_require__(/*! ./message */ \"./src/protocol-v4/connection/message.js\");\nconst { ConnectionRequest } = __webpack_require__(/*! ./request */ \"./src/protocol-v4/connection/request.js\");\nconst { ConnectionResponse } = __webpack_require__(/*! ./response */ \"./src/protocol-v4/connection/response.js\");\nconst { ConnectionBatch } = __webpack_require__(/*! ./batch */ \"./src/protocol-v4/connection/batch.js\");\nconst { ConnectionAck } = __webpack_require__(/*! ./ack */ \"./src/protocol-v4/connection/ack.js\");\nconst { ErrorPayloadCtorFactory } = __webpack_require__(/*! ./error-payload */ \"./src/protocol-v4/connection/error-payload.js\");\nconst isDebugBuild = __webpack_require__(/*! dcp/common/dcp-build */ \"./src/common/dcp-build.js\").build === 'debug';\n\nlet globalConnectionId = 0;\n\nconst CONNECTION_STATES = [\n 'initial',\n 'connecting',\n 'established',\n 'waiting', /* Targets only, when send fails */\n 'close-wait', /* Target of close message is in this state until response is acknowledged */\n 'closing',\n 'closed',\n]\n\nclass Connection extends EventEmitter {\n static get VERSION() {\n return '5.0.0'; // Semver format\n }\n\n static get VERSION_COMPATIBILITY() {\n return '5.0.0'; // Semver format, can be a range\n }\n\n /**\n * @constructor Connection form 4:\n * Create a DCP Connection object for an initiator.\n * @param {string} target The string version (ie href) of the URL of the target to connect to.\n * @param {wallet.IdKeystore} [idKeystore]\n * @param {Object} [connectionOptions]\n * @see form 1\n */\n /**\n * @constructor Connection form 3:\n * Create a DCP Connection object for an initiator.\n * @param {DcpURL|URL} target The URL of the target to connect to.\n * @param {wallet.IdKeystore} [idKeystore]\n * @param {Object} [connectionOptions]\n * @see form 1\n */\n /**\n * @constructor Connection form 2:\n * Create a DCP Connection object for a target.\n * @param {wallet.IdKeystore} [idKeystore]\n * @param {Object} [connectionOptions]\n * @see form 1\n */\n /**\n * @constructor Connection form 1\n * Create a DCP Connection object. \n * \n * @note Connection objects exist for the lifetime of a given DCP connection \n * (session), whether or not the underlying transport (eg internet protocol) is connected or not. Once \n * the DCP session has ended, this object has no purpose and is not reusable.\n * \n * @param {object|undefined} target Undefined when we are the target, or an object describing the target. This object \n * may contain the following properties; 'location' is mandatory:\n * - location: a DcpURL that is valid from the Internet\n * - friendLocation: a DcpURL that is valid from an intranet; if\n * both location and friendLocation specified, the best\n * one will be chosen by examining IP addresses\n * - identity: a object with an address property which is an\n * instanceof wallet.Address which corresponds to the peer's\n * identity; this overrides the identity cache unless\n * connectionOptions.strict is truey.\n * @param {wallet.IdKeystore} [idKeystore] The keystore used to sign messages; used for non-repudiation.\n * If not specified, a dynamically-generated keystore will be used.\n * \n * @param {Object} [connectionOptions] Extra connection options that aren't defined via dcpConfig.dcp.connectionOptions.\n * These options include:\n * - identityUnlockTimeout: Number of (floating-point) seconds to leave the identity \n * keystore unlocked between invocations of Connection.send\n */\n constructor(target, idKeystore, connectionOptions)\n {\n /* polymorphism strategy: rewrite all to form 1 before super */\n if (target instanceof wallet.Keystore) /* form 2 */\n { \n connectionOptions = arguments[2];\n idKeystore = arguments[1];\n target = undefined;\n }\n if (typeof connectionOptions === 'undefined')\n connectionOptions = {};\n\n if (target instanceof URL) /* form 3.2 */\n target = { location: new DcpURL(target) };\n else if (DcpURL.isURL(target)) /* form 3.1 */\n target = { location: new DcpURL(target) };\n else if (target instanceof String || typeof target === 'string') /* form 4 */\n target = { location: new DcpURL(target) };\n\n assert((typeof target === 'undefined') || (typeof target === 'object' && DcpURL.isURL(target.location)));\n assert(typeof connectionOptions === 'object');\n \n super(`Protocol Connection (${target ? 'initiator' : 'target'})`);\n\n if (target) {\n this.isInitiator = true;\n this.debugLabel = 'connection(i):';\n this._target = target;\n } else {\n this.isInitiator = false;\n this.debugLabel = 'connection(t):';\n }\n\n if (idKeystore) {\n this.identityPromise = (async () => await idKeystore)();\n } else {\n this.identityPromise = wallet.getId();\n }\n\n this.identityPromise.then((keystore) => {\n this.identity = keystore;\n debugging('connection') && console.debug(this.debugLabel, 'identity is', keystore.address);\n return keystore;\n });\n\n this.ttl = {\n ntp: false,\n }\n \n // Init internal state / vars\n this.state = new Synchronizer(CONNECTION_STATES[0], CONNECTION_STATES);\n this.state.on('change', (s) => this.emit('readyStateChange', s) );\n\n this._id = globalConnectionId++;\n this.debugLabel = this.debugLabel.replace(')', `#${this._id})`);\n debugging('connection') && console.debug(this.debugLabel, 'connection id is', this._id, `target is ${target && target.location}`);\n this.dcpsid = null;\n this.peerAddress = null;\n this.transport = null;\n this.maxConnectionTimeout = null;\n this.messageFactory = new MessageFactory(this);\n this.messageLedger = new MessageLedger(this);\n\n this.Message = ConnectionMessage(this);\n this.Request = ConnectionRequest(this.Message);\n this.Response = ConnectionResponse(this.Message);\n this.Batch = ConnectionBatch(this.Message);\n this.Ack = ConnectionAck(this.Message);\n this.ErrorPayload = ErrorPayloadCtorFactory(this);\n this.connectTime = Date.now();\n\n this.openRequests = {};\n ValidityStampCache.init();\n\n this.receiver = new Receiver(this, this.messageLedger);\n this.receiver.on('request', (req) => this.emit('request', req));\n\n debugging('connection') && console.debug(this.debugLabel, `new; ${target && target.location || '<target>'}`);\n\n this.configureConnectionForUrl = (url) => {\n if (typeof connectionOptions.ttl === 'number')\n connectionOptions = Object.assign({}, connectionOptions, { ttl: { default: connectionOptions.ttl } });\n \n this.url = url;\n this.connectionOptions = leafMerge(/* ordered from least to most specific */\n dcpConfig.dcp.connectionOptions.default,\n this.url && dcpConfig.dcp.connectionOptions[this.url.hostname],\n this.url && dcpConfig.dcp.connectionOptions[this.url.origin],\n dcpConfig.dcp.connectionOptions[this.isInitiator ? this.url.href : 'target'],\n connectionOptions\n );\n\n // ensure we're using a _copy_ of the transports list, not mutating the original\n this.connectionOptions.transports = [...this.connectionOptions.transports];\n this.originalConnectionTransports = [].concat(this.connectionOptions.transports);\n this.transportsTried = 0;\n\n this.ttl = leafMerge(this.ttl, this.connectionOptions.ttl);\n this.unlockTimeout = this.connectionOptions.identityUnlockTimeout;\n this.connectionOptions.id = this._id;\n this.EBOIterator = makeEBOIterator(1000, 60000);\n\n assert(this.unlockTimeout >= 0);\n assert(typeof this.connectionOptions.ttl.min === 'number');\n assert(typeof this.connectionOptions.ttl.max === 'number');\n assert(typeof this.connectionOptions.ttl.default === 'number');\n\n this.secureLocation = determineIfSecureLocation(this);\n }\n }\n\n /**\n * This method is an instantiator/factory function for building a connection\n * that will act as the target in a new protocol connection. It's a little\n * like making a new connection and calling `connect` on it, except that\n * instead of having a url to connect to we have a transport which should\n * be ready to emit the connect message from the initiator.\n * \n * @param {wallet.Keystore} ks - Keystore to associate to the new connection.\n */\n static async newTarget(url, ks, transport) {\n const target = new Connection(undefined, ks, {\n ttl: {\n ntp: true, // targets should always be NTP-synced\n }\n });\n\n target.configureConnectionForUrl(url);\n assert(target.connectionOptions.ttl.ntp);\n \n await target.identityPromise;\n target.transport = transport;\n target.sender = new Sender(target);\n target.state.set('initial', 'connecting');\n return target;\n }\n\n /**\n * When invoked by the initator, this method establishes the connection by connecting\n * to the target provided to the constructor.\n * \n * When a dcpConfig-fragment is used as the target object, we determine automatically if \n * we should be using the location or the friendLocation property at this point.\n *\n * This function is also where the connectionOptions are merged together. This means \n * that if either of these objects come from dcpConfig and we HUP a service running this\n * code, that the new configuration will be used to connect, instead of the configuration \n * that was defined at the time of construction.\n *\n * @throws when called by a target, or when called more than once\n */\n async connect() {\n var presharedPeerAddress;\n \n let transportName;\n \n if (!this.state.in(['initial', 'connecting'])) {\n throw new Error(`Can only invoke connect in initial/connecting state. Current state: ${this.state.valueOf()}.`, );\n }\n\n // make connect re-entrant if called during the connecting phase\n if (this.state.is('connecting'))\n {\n /* nth connect attempt on same Connection instance before actual connection -- swallow attempts and wait until connected */\n do\n {\n await this.state.until(['established', 'close-wait', 'closing', 'closed']);\n } while(this.state.is('waiting'));\n\n if (!this.state.is('established'))\n throw new Error(`Could not establish connection to ${this.url}; in state ${this.state}`);\n return;\n }\n\n this.state.set('initial', 'connecting');\n\n if (this._target && this._target.hasOwnProperty('friendLocation') && await isFriendlyURL(this._target.friendLocation))\n this.configureConnectionForUrl(this._target.friendLocation);\n else\n this.configureConnectionForUrl(this._target.location);\n\n const transport = this.fetchTransport();\n this.transport = transport.transport;\n transportName = transport.moduleName;\n this.sender = new Sender(this);\n // create sender before promises so that we can still enqueue messages before hopping off the event loop\n await this.identityPromise;\n await this.connectToTarget(transportName);\n const establishResults = await this.sender.establish().catch( (err) => {\n console.error('Failed to establish connection.');\n this.close(err, true);\n throw err;\n });\n const dcpsid = establishResults.dcpsid;\n const peerAddress = wallet.Address(establishResults.peerAddress);\n\n if (!this.connectionOptions.strict && this._target.identity)\n {\n if (determineIfSecureConfig())\n {\n presharedPeerAddress = (await this._target.identity).address;\n debugging('connection') && console.debug('Using preshared peer address', presharedPeerAddress);\n }\n }\n this.ensureIdentity(peerAddress, presharedPeerAddress); /** XXXwg possible resource leak: need cleanup; need try {} catch->emit(cleanup) */\n \n // checks have passed, now we can set props\n this.peerAddress = peerAddress;\n if (this.dcpsid)\n throw new DCPError(`Reached impossible state in connection.js; dcpsid already specified ${this.dcpsid} (${this.url})`, 'DCPC-1004');\n this.dcpsid = dcpsid;\n \n // Update state\n this.state.set('connecting', 'established'); /* established => dcpsid has been set */\n this.emit('connected', this.url);\n this.sender.finishSetup();\n }\n\n moreTransportsExist() {\n return this.connectionOptions\n && this.connectionOptions.transports\n && Array.isArray(this.connectionOptions.transports)\n && this.connectionOptions.transports.length > 0;\n }\n \n /**\n * Initiators only.\n * Try to get and establish a transport from the transports list.\n * @returns {Transport} a new transport instance or false if no transports are left to try\n */\n fetchTransport() {\n //We want to cycle our possible transports until the maxConnectionTimeout runs out or we have a connection\n const moduleName = this.connectionOptions.transports.shift();\n this.connectionOptions.transports.push(moduleName);\n this.transportsTried++;\n debugging() && console.log(this.debugLabel, 'fetchTransport trying ', moduleName, `remaining transports: ${this.connectionOptions.transports.length}`)\n if (!moduleName) {\n console.error('Error: All transports have failed to connect/transmit within limits. Closing connection.');\n this.close('all transports failed', true).catch(error => {\n console.error(`Error: failed to close transport:`, error);\n });\n throw new DCPError(`Failed to connect to ${this.url}, no transport could be established`, 'ENOTRANSPORT');\n }\n const TransportClass = Transport.require(moduleName);\n const transport = new TransportClass(null, this.connectionOptions);\n transport.on('message', (m) => {\n this.onMessage(JSON.parse(m))\n });\n return { transport, moduleName };\n }\n\n /**\n * Initiators only\n * Resolves when we have connected to target using a transport.\n * Calls \"failTransport\" if we run out of time / attempts to connect.\n * Let's transport handle failure but also is watching for it too.\n */\n async connectToTarget(currentTransport) {\n debugging('connection') && console.log(this.debugLabel, `Using transport \"${this.transport.name}\" ... connecting to ${this.url}.`);\n const globalWaitMs = dcpConfig.dcp.maxConnectionTimeout;\n const connectWaitMs = this.transport.connect(this.url, this.connectionOptions);\n\n this.maxConnectionTimeout = this.maxConnectionTimeout ? this.maxConnectionTimeout\n : setTimeout(this.close.bind(this, `Timeout to have a transport connect reached at ${globalWaitMs} ms`, true), globalWaitMs);\n\n await new Promise( (resolve) => {\n // Listener for a connect-failed, will invoke EBO algorithm to retry the connection\n this.transport.once('connect-failed', () => {\n if (this.transportsTried >= this.connectionOptions.transports.length)\n {\n this.transportsTried = 0;\n const backoffTime = this.EBOIterator.next().value;\n debugging('connection') && console.debug(this.debugLabel, 'connecting in', backoffTime / 1000, 'seconds');\n this.failTransport('Connect-failed event handler fired.', backoffTime);\n }\n else\n {\n this.failTransport('Connect-failed event handler fired.');\n }\n });\n\n this.transport.once('connected', () => {\n clearTimeout(this.maxConnectionTimeout);\n this.maxConnectionTimeout = null;\n /* When we successfully connect a transport, we know that transport has potential to consistently work,\n * and disconnects may happen for unrelated reason (such as switching wifi causing a disconnect). \n * Thus, we recreate our transport options list with this transport at the top\n */\n this.connectionOptions.transport = [currentTransport].concat(this.originalConnectionTransports);\n resolve();\n });\n });\n this.transport.on('reconnect', () => {\n debugging('connection') && console.log(this.debugLabel, `Transport reconnected to ${this.url}`);\n // Send a keepalive to the remote end, to establish the new Transport with this Connection\n this.keepalive();\n this.emit('reconnect');\n });\n }\n\n /**\n * Target gains full status once dcpsid and peerAddress\n * are provided by first connect request.\n * @param {string} dcpsid dcpsid\n * @param {wallet.Address} peerAddress Address of peer\n */\n establishTarget(dcpsid, peerAddress) {\n this.connectResponseId = Symbol(); // un-register ConnectResponse\n this.peerAddress = peerAddress;\n if (this.dcpsid)\n throw new DCPError(`Reached impossible state in connection.js; dcpsid already specified ${this.dcpsid}!=${dcpsid} (${this.url})`, 'DCPC-1005');\n this.dcpsid = dcpsid;\n this.state.set('connecting', 'established'); /* established => dcpsid has been set */\n }\n\n ensureIdentity (peerAddress, presharedPeerAddress) {\n let identityCache = getGlobalIdentityCache();\n \n if (!identityCache.validateIdentity(this.url.href, peerAddress)) {\n if (presharedPeerAddress)\n {\n console.error(`**** Warning: preshared peer address ${presharedPeerAddress} does not match address ${identityCache.getIdentity(this.url.href)} provided for ${this.url} in identity cache ****`);\n __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\").sleep(10);\n identityCache.insert(this.url.href, peerAddress);\n }\n throw new DCPError(`Identity address ${peerAddress} does not match the saved key for ${this.url}`,'EADDRCHANGE');\n }\n }\n\n /**\n * Check that the transport has given us a message worth dealing with then\n * either let the receiver handle it (message) or the message ledger (ack).\n * @param {object} message parsed (but not validated) message object\n */\n async onMessage (message) {\n if (this.state.is('closed')) {\n debugging('connection') && console.warn(this.debugLabel, 'onMessage was called on a closed connection.');\n return;\n }\n /**\n * We always ack a duplicate transmission.\n * This must happen before validation since during startup we may lack a\n * nonce or dcpsid (depending on whether initiator or target + race).\n */\n if (this.isDuplicateTransmission(message)) {\n debugging('connection') && console.debug(this.debugLabel, 'duplicate message:', message.body);\n this.transport.send(this.lastAckSigned);\n return;\n }\n\n let messageIsValid = false;\n try {\n messageIsValid = this.isMessageValid(message);\n } catch (error) {\n console.error('Message passed to validator threw an Error:', error);\n }\n /* messageIsValid with either be true, or a string with the reason the message isn't valid*/\n if (messageIsValid !== true) {\n if (debugging('connection'))\n this.close(`invalid message: ${messageIsValid}\\n ${JSON.stringify(message)}`, true);\n else\n this.close(`invalid message: ${messageIsValid}`, true);\n return;\n }\n\n if (message.body.type === \"ack\") {\n const ack = new this.Ack(message.body);\n this.messageLedger.handleAck(ack);\n return;\n } else if (message.body.type !== 'unhandled-message') {\n this.lastMessage = message;\n await this.ackMessage(message);\n }\n this.receiver.onMessage(message);\n }\n\n async ackMessage(message) {\n debugging('connection') && console.debug(this.debugLabel, 'acking message.');\n const ack = new this.Ack(message);\n const signedMessage = await ack.sign(this.identity);\n this.transport.send(signedMessage);\n this.lastAck = ack;\n this.lastAckSigned = signedMessage;\n }\n\n /**\n * Checks if the batch we just received has the same nonce\n * as the most-recently received batch.\n * @param {object} messageJSON\n */\n isDuplicateTransmission(messageJSON) {\n return this.lastMessage && this.lastMessage.body.nonce === messageJSON.body.nonce;\n }\n\n /**\n * This method is used to perform validation on all types of messages.\n * It validates the signature, DCPSID, nonce, and the peerAddress.\n * @param {Object} message\n */\n isMessageValid(message) {\n const owner = new wallet.Address(message.owner);\n const signatureValid = owner.verifySignature(message.body, message.signature);\n // keeps track of the connection url so that if the message is invalid, the source can be traced easily\n // if message is invalid, it will return the reason for the error plus the URL, otherwise will return true.\n let currURL = \"Could not retrieve connection URL\"\n if (this.url) {\n currURL = this.url.href;\n }\n if (!signatureValid) {\n debugging('connection') && console.warn(\"Message has an invalid signature, aborting connection\");\n return \"invalid message signature \" + currURL;\n } else if (message.body.type === 'unhandled-message') {\n // this special message type may not have a dcpsid, peerAddress, etc.\n debugging('connection') && console.warn(\"Target Error - target could not process message.\", JSON.stringify(message.body),\n \"Aborting connection.\");\n return \"target could not process message \" + currURL;\n } else if (this.state.in(['established', 'closing', 'close-wait'])) {\n const body = message.body;\n\n /**\n * Security note:\n * We don't require the dcpsid to match on an ack because the connect response\n * ack doesn't have a dcpsid until after it is processed. Also ack's are protected\n * by ack tokens and signatures, so this doesn't leave a hole, just an inconsistency.\n */\n if (message.body.type !== 'ack' && body.dcpsid !== this.dcpsid) {\n debugging('connection') && console.warn(\"Received message's DCPSID does not match, aborting connection\\n\",\n \"Wallet owner:\", JSON.stringify(owner), '\\n',\n \"(ours)\", this.dcpsid, (Date.now() - this.connectTime)/1000, \"seconds after connecting - state:\", this.state._, \"\\n\", \n \"(theirs)\", body.dcpsid);\n if(body.dcpsid.substring(0, body.dcpsid.length/2) !== this.dcpsid.substring(0, this.dcpsid.length/2)){\n debugging('connection') && console.warn(\" Left half of both DCPSID is different\");\n }\n if(body.dcpsid.substring(body.dcpsid.length/2 + 1, body.dcpsid.length) !== this.dcpsid.substring(this.dcpsid.length/2 + 1, body.dcpsid.length)){\n debugging('connection') && console.warn(\" Right half of both DCPSID is different\");\n }\n debugging('connection') && console.log(\"The full message that caused this error:\");\n debugging('connection') && console.log(JSON.stringify(message, null, 2));\n debugging('connection') && console.log(\"The stack up to this point is:\");\n debugging('connection') && console.trace();\n debugging('connection') && console.log(\"The target has address:\")\n debugging('connection') && console.log(JSON.stringify(this.peerAddress));\n debugging('connection') && console.log(\"The transport is:\");\n debugging('connection') && console.log(this.transport);\n return \"DCPSID do not match\";\n } else if (!this.peerAddress.eq(owner)) {\n debugging('connection') && console.warn(\"Received message's signature address does not match peer address, aborting connection\\n\",\n \"(signature addr)\", owner, '\\n',\n \"(peer addr)\", this.peerAddress);\n return \"received message signature does not match peer address\";\n } else if (message.body.type !== 'ack' && this.lastAck.nonce !== message.body.nonce) {\n debugging('connection') && console.warn(\"Received message's nonce does not match expected nonce, aborting connection\\n\");\n debugging('connection') && console.debug(this.debugLabel, this.lastAck.nonce, message.body.nonce);\n return \"received message's nonce does not match expected nonce \" + currURL;\n }\n } else {\n // only validate signature for unestablished connections\n // while connecting, the target gets its nonce from the initiator\n assert(this.state.is('connecting'));\n if (!this.isInitiator) this.sender.nonce = message.body.nonce;\n }\n\n return true;\n }\n\n /**\n * Targets Only.\n * The receiver creates a special connect response and the connection\n * needs to know about it to get ready for the ack. See `isWaitingForAck`.\n * @param {Message} message message we are sending out and waiting to\n * ack'd, probably a batch containing the response.\n */\n registerConnectResponse(message) {\n this.connectResponseId = message.id;\n }\n\n /**\n * Targets only\n * During the connection process a target sends a connect\n * response to an initiator and the initiator will ack it. Since transports\n * are not tightly coupled, we have no authoritative way to route the ack back\n * to the right connection. So a connection briefly registers the ack it\n * is looking for in this case. It will formally validate the ack after routing.\n * @param {string} messageId id of the message this ack is acknowledging.\n */\n isWaitingForAck(messageId) {\n return messageId === this.connectResponseId;\n }\n\n /**\n * Targets: nothing to do, we are waiting for initiator.\n * Initiators: give up on current transport and try next available option.\n * For now we just have socketio so that means we throw an error.\n * @return {Promise<boolean>} indicating whether we were able to find a new transport.\n */\n async failTransport (failureDescription, retryWaitTime = 0) {\n let transportName;\n const preWaitingState = this.state.valueOf(); /* XXXwg bogus - need explicit and reasonable source states */\n\n /* If we are already in process of closing or already waiting, we don't want to beat \n * the dead horse. Let it stay dead with no transport\n */\n if (['closing', 'closed', 'close-wait', 'waiting'].includes(preWaitingState))\n return;\n\n this.state.testAndSet(preWaitingState, 'waiting');\n debugging('connection') && console.log(this.debugLabel, `Transport \"${this.transport.name}\" has failed.`);\n \n if (this.isInitiator) {\n if (!this.moreTransportsExist()) {\n console.error('Transport failed:', failureDescription);\n // fetchTransport will immediately fail\n }\n debugging('connection') && console.log(this.debugLabel, `Attempting to use next transport.`);\n this.transport.close();\n const transport = this.fetchTransport();\n this.transport = transport.transport;\n transportName = transport.moduleName;\n \n await new Promise(r => setTimeout(r, retryWaitTime));\n\n this.state.set('waiting', preWaitingState);\n await this.connectToTarget(transportName);\n }\n }\n\n /**\n * Targets only\n * Give target another transport to try sending messages on again.\n */\n revive(transport) {\n this.transport = transport;\n this.state.set('waiting', 'established');\n }\n\n /**\n * Put connection into close-wait state so that a call to `close`\n * in this state will *not* trigger sending a `close` message to the peer.\n * Then call close once the passed promise resolves.\n *\n * @note: This function is called when the remote end of the transport sends\n * a close command, and the waitPromise in that case is the response.\n */\n async closeWait (waitPromise)\n {\n const preCloseState = this.state.valueOf();\n\n debugging('connection') && console.debug(this.debugLabel, `responding to close. state=closeWait dcpsid=${this.dcpsid}`);\n\n if (this.state.is('closed'))\n {\n debugging('connection') && console.debug(this.debugLabel, `remote asked us to close a closed connection; dcpsid=${this.dcpsid}`);\n return;\n }\n\n // wait for waitPromise to resolve, reject if not within time limit\n try {\n await new Promise((resolve, reject) => {\n const closeWaitTimeout = setTimeout(() => {\n reject(new Error('Connection.closeWait timed out'));\n }, 10000); /* hardcode 10s wait /wg mar 2021 */\n waitPromise.then(resolve, reject).finally(() => clearTimeout(closeWaitTimeout));\n })\n debugging('connection') && console.debug(this.debugLabel, `close response ACK'd; closing. dcpsid=${this.dcpsid}`);\n } catch (error) {\n debugging('connection') && console.debug(this.debugLabel, `${error.message}; closing. dcpsid=${this.dcpsid}`);\n }\n // continue with close in either case\n let reason = 'received close from peer';\n if (this.url)\n reason += ` (${this.url})`;\n\n // If we're already closing, wait for it to complete then resolve\n // WARNING: any place we transition to closing or close-wait, we MUST guarantedd\n // that 'end' will be emitted, or this code will hang forever!\n if (this.state.in(['close-wait', 'closing'])) {\n return new Promise((resolve) => {\n this.once('end', resolve) /* eventually fired by doClose elsewhere */\n });\n }\n\n if (this.state.is('closed')) /* closed somehow on us during await */\n return;\n\n this.state.set(preCloseState, 'close-wait');\n return this.doClose(preCloseState, reason, true);\n }\n\n /**\n * This method will begin gracefully closing the protocol connection.\n * It will only close after sending all pending messages.\n * \n * @param {string|Error} [reason] Either an Error or a message to use in the Error that will reject pending sends.\n * @param {boolean} [immediate] If true, does not wait to send messages or the `close` request.\n *\n * @return a Promise which resolves when the connection has been confirmed closed and the end event has been fired.\n */\n close (reason=null, immediate=false)\n {\n if (this.state.is('closed')) return;\n\n const preCloseState = this.state.valueOf();\n debugging('connection') && \n console.debug(this.debugLabel, \n `close; dcpsid=${this.dcpsid} state=${preCloseState} reason=${reason} immediate=${immediate} stack=${new Error().stack}`);\n\n // If we're already closing, wait for it to complete then resolve\n if (this.state.in(['close-wait', 'closing'])) {\n return new Promise((resolve) => {\n this.once('end', resolve)\n });\n }\n\n // Put in closing state no matter the current state\n this.state.set(preCloseState, 'closing');\n\n // Perform actual work of closing\n return this.doClose(preCloseState, reason, immediate);\n }\n\n /**\n * sends close message to peer and waits for response\n * @note: This function is not reentrant!\n */\n async closeGracefully() {\n await new Promise((resolve, reject) => {\n // reject if we don't resolve within time limit\n const timeoutTimer = setTimeout(() => {\n reject(new DCPError('Connection.close timed out', 'DCPC-1003'));\n }, this.connectionOptions.closeTimeout * 1000);\n \n if (this.transport)\n {\n /* If we got as far as initializing a transport during connect(), send close\n * message to peer, should get a response before time is up.\n */\n const closeMessage = this.messageFactory.buildMessage('close');\n this.sender.enqueue(closeMessage).then(resolve, reject).finally(() => clearTimeout(timeoutTimer));\n }\n })\n // hop off event loop so that close response ack can get out\n await new Promise(r=>setTimeout(r));\n }\n\n /**\n * Core close functionality shared by `close` and `closeWait`\n *\n * @param {string} preCloseState the state that the connection was in at the start of the\n * invocation of close() or closeWait()\n *\n * @note: this function is not reentrant due to closeGracefully\n */\n async doClose(preCloseState, reason, immediate) {\n const dcpsid = this.dcpsid;\n\n try\n {\n assert(this.state.valueOf() === 'closing' || this.state.valueOf() === 'close-wait');\n \n // Emit the close event the moment we know we are going to close, \n // so we can catch the close event and reopen the connection\n this.emit('close', dcpsid /* should be undefined in initial state */);\n \n if (preCloseState === 'established' && !immediate) {\n try {\n await this.closeGracefully();\n } catch(e) {\n debugging() && console.warn(`Warning: could not close connection gracefully. connectionid=${this._id}, dcpsid=,${this.dcpsid}, url=${this.url ? this.url.href : 'unknown url'} - (${e.message})`);\n }\n }\n\n // can delete these now that we've sent the close message\n this.dcpsid = null;\n this.peerAddress = null;\n\n /* build error message */\n let rejectErr;\n if (reason instanceof Error) {\n rejectErr = reason;\n } else {\n let message;\n if (typeof reason === 'string' || reason instanceof String ) {\n message = reason;\n } else {\n if (this.url)\n message = `Connection closed (url: ${this.url}, dcpsid: ${dcpsid})`;\n else\n message = `Connection closed (dcpsid: ${dcpsid})`;\n }\n rejectErr = new DCPError(message, 'DCPC-1002');\n }\n \n if (preCloseState !== 'initial')\n {\n // If we are closing the connection, we want to resolve all of the .send() promises with success = false */\n // Reject any pending transmissions in the message ledger\n this.messageLedger.silentFailMessage = true;\n this.messageLedger.failAllTransmissions(rejectErr);\n \n if (this.transport)\n {\n try { this.sender.shutdown(); }\n catch(e) { debugging() && console.warn(`Warning: could not shutdown sender; dcpsid=,${dcpsid}`, e); }\n\n try { this.transport.close(); }\n catch(e) { debugging() && console.warn(`Warning: could not close transport; dcpsid=,${dcpsid}`, e); }\n }\n }\n\n this.state.setIf('closing', 'closed');\n this.state.setIf('close-wait', 'closed');\n }\n finally\n {\n this.emit('end'); /* end event resolves promises on other threads for closeWait and close */\n }\n }\n\n /**\n * Sends a message to the connected peer. If the connection has not yet been established,\n * this routine will first invoke this.connect().\n * \n * @param {...any} args\n * @returns {Promise<Response>} a promise which resolves to a response.\n */\n async send(...args) {\n if (this.state.in(['initial','connecting'])) {\n // Original message will be rejected so we don't want a second reject\n // here since it would be uncaught.\n await this.connect().catch( e => {\n throw new DCPError(`Connection (${this._id}) failed to connect to ${this.url ? this.url.href : '<unknown url>'}: ${e.message}`, e.code);\n });\n }\n\n if (this.state.in(['closing', 'closed'])) {\n let e = new DCPError(`Connection (${this._id}) in state '${this.state}' cannot send. (url: ${this.url})`, 'DCPC-1001');\n throw e;\n }\n\n const message = this.messageFactory.buildMessage(...args);\n return this.sender.enqueue(message);\n }\n\n /**\n * Sends a v3-over-v4 request\n * @param url {string} v3 DcpURL\n * @param message {object} v3 \"message\" value; may be empty\n * @param authKey {object} (v3 signing|v4 authorizing) keystore, if different from identity\n * @return {Promise<object>} Resolves with v3 route's response data\n */\n async sendv3(url, message = {}, authKey = null) {\n const v3Data = {\n url,\n message,\n };\n \n const req = new this.Request('v3', v3Data);\n\n if (authKey && !this.identity.address.eq(authKey.address)) {\n await req.authorize(authKey);\n }\n \n let response = await req.send()\n\n .catch((error) => {\n return {\n success: false,\n payload: error,\n }\n });\n\n // fail at v4-protocol-level? throw it\n if (!response.success) {\n throw response.payload;\n }\n \n // fail at v3-level? also throw it\n if (response.payload.v3status === 'reject')\n throw response.payload.v3rejection;\n \n return response.payload.v3resolution;\n }\n\n /**\n * This routine returns the current time for the purposes of\n * populating the Request message payload.validity.time property.\n * \n * @returns {Number} the integer number of seconds which have elapsed since the epoch\n */\n currentTime() {\n let msSinceEpoch;\n if (this.ttl.ntp) {\n msSinceEpoch = Date.now();\n } else {\n const msSinceLastReceipt = perf.now() - this.receiver.lastResponseTiming.receivedMs;\n msSinceEpoch = this.receiver.lastResponseTiming.time * 1000 + msSinceLastReceipt;\n }\n return Math.floor(msSinceEpoch / 1000);\n }\n\n /**\n * This method sends a keepalive to the peer, and resolves when the response has been received.\n */\n keepalive() {\n return this.send('keepalive');\n }\n\n /**\n * This method checks to see a service connection is established, typically will be given a timeOut condition when invoked\n */\n async setCheckConnectionTimeout(source) {\n // failTimeout is checked to account for a race condition if the connection is established at the same time as the fail timeout expires \n let failTimeout = false;\n function checkConnectionEstablished() {\n if(failTimeout){\n failTimeout = false;\n throw new DCPError(`Connection to ${this.url} from ${source} not established within 30 seconds, instead is in state ${this.state}`, 'DCP-1006');\n }\n }\n failTimeout = setTimeout(checkConnectionEstablished.bind(this), 30000);\n this.once('connected', () => {\n if(failTimeout){\n clearTimeout(failTimeout);\n failTimeout = false;\n }\n });\n await this.connect();\n }\n\n}\n\n/**\n * Returns true if friendLocation should work in place of location from this host.\n * This allows us to transparently configure inter-daemon communication that uses\n * local LAN IPs instead of bouncing off the firewall for NAT.\n */\nasync function isFriendlyURL(url)\n{\n var remoteIp, dnsA;\n var ifaces;\n \n if (url.hostname === 'localhost')\n return true;\n\n switch(url.protocol)\n {\n case 'http:':\n case 'https:':\n case 'ws:':\n case 'tcp:':\n case 'udp:':\n case 'dcpsaw:':\n break;\n default:\n return false;\n }\n\n /* Consider same-origin match friendly */\n if (__webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\").isBrowserPlatform)\n return url.origin === window.location.origin;\n\n /* Convert an IP address to a 32-bit int in network order */\n function i32(addr)\n {\n var ret = 0;\n var octets = addr.split('.');\n\n ret |= octets[0] << 24; /* Note: JS ints are signed 32, but that doesn't matter for masking */\n ret |= octets[1] << 16;\n ret |= octets[2] << 8;\n ret |= octets[3] << 0;\n\n return ret;\n }\n \n /* Consider machines in same IPv4 subnet friendly */\n dnsA = await requireNative('dns').promises.lookup(url.hostname, {family: 4});\n if (!dnsA)\n return false;\n remoteIp = i32(dnsA.address);\n ifaces = requireNative('os').networkInterfaces();\n for (let ifaceName of Object.keys(ifaces))\n {\n for (let alias of ifaces[ifaceName])\n {\n if (alias.family != 'IPv4')\n continue;\n\n let i32_addr = i32(alias.address);\n let i32_mask = i32(alias.netmask);\n\n if ((i32_addr & i32_mask) === (remoteIp & i32_mask))\n return true;\n }\n }\n\n return false;\n}\n\n/** \n * Determine if we got the scheduler config from a secure source, eg https or local disk.\n * We assume that all https transactions have PKI-CA verified.\n *\n * @note protocol::getSchedulerConfigLocation() is populated via node-libs/config.js or dcp-client/index.js\n *\n * @returns true or falsey\n */\nfunction determineIfSecureConfig()\n{\n var schedulerConfigLocation = __webpack_require__(/*! dcp/protocol-v4 */ \"./src/protocol-v4/index.js\").getSchedulerConfigLocation();\n var schedulerConfigSecure;\n \n if (schedulerConfigLocation && (schedulerConfigLocation.protocol === 'https:' || schedulerConfigLocation.protocol === 'file:'))\n {\n debugging('strict-mode') && console.log(`scheduler config location ${schedulerConfigLocation} is secure`); /* from casual eavesdropping */\n schedulerConfigSecure = true;\n }\n\n if (isDebugBuild)\n {\n debugging('strict-mode') && console.log(`scheduler config location is always secure for debug builds`);\n schedulerConfigSecure = 'debug';\n }\n\n debuggingScheduler() && console.debug(`Config Location ${schedulerConfigLocation} is ${!schedulerConfigSecure ? 'not secure' : 'secure-' + schedulerConfigSecure}`);\n return schedulerConfigSecure;\n}\n\n/**\n * Determine if a URL is secure by examinining the protocol, connection, and information about the \n * process; in particular, we try to determine if the dcp config was securely provided, because if \n * it wasn't, then we can't have a secure location, since the origin could be compromised.\n * \n * \"Secure\" in this case means \"secure against casual eavesdropping\", and this information should only\n * be used to refuse to send secrets over the transport or similar.\n *\n * @returns true or falsey\n */\nfunction determineIfSecureLocation(conn)\n{\n var isSecureConfig = determineIfSecureConfig();\n var secureLocation;\n \n if (!isSecureConfig) /* can't have a secure location without a secure configuration */\n return null;\n \n if (isDebugBuild || conn.url.protocol === 'https:' || conn.url.protocol === 'tcps:')\n secureLocation = true;\n else if (conn.isInitiator && conn._target.hasOwnProperty('friendLocation') && conn.url === conn._target.friendLocation)\n secureLocation = true;\n else if (conn.connectionOptions.allowUnencryptedSecrets)\n secureLocation = 'override';\n else\n secureLocation = false;\n\n (debugging() || debuggingScheduler()) && console.debug(`Location ${conn.url} is ${!secureLocation ? 'not secure' : 'secure-' + secureLocation}`);\n \n return secureLocation;\n}\n\nexports.Connection = Connection;\n\n\n//# sourceURL=webpack:///./src/protocol-v4/connection/connection.js?");
5508
5508
 
5509
5509
  /***/ }),
5510
5510
 
@@ -5560,7 +5560,7 @@ eval("/**\n * @file protocol/connection/message-factory.js\n * @author
5560
5560
  /*! no static exports found */
5561
5561
  /***/ (function(module, exports, __webpack_require__) {
5562
5562
 
5563
- eval("/**\n * @file protocol/connection/message-ledger.js\n * @author Ryan Rossiter, ryan@kingsds.network\n * @date January 2020\n *\n * The MessageLedger is responsible for storing receipts for messages\n * that are given to the sender. The receipts are used to resolve the\n * promise that was returned from the sender, to indicate when the message\n * is complete.\n * \n * Request receipt promises are resolved once a response has been received.\n * Response receipt promises are resolved once the response has been sent.\n */\nconst debugging = __webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope('dcp');\nconst ONE_DAY_MS = 24 * 60 * 60 * 1000;\nclass MessageLedger {\n constructor(connection) {\n this.connection = connection;\n const id = this.connection._id;\n this.debugLabel = connection.isInitiator? `message-ledger(i#${id}):` : `message-ledger(t#${id}):`;\n this.receipts = new Map(); // id => {message, resolve, reject\n this.batches = new Map(); // id -> [...messages]\n this.silentFailMessage = false;\n }\n\n /**\n * This method is invoked by the sender upon sending a new request.\n * It stores a receipt of the message and returns a promise that will\n * be resolved when the message's lifecycle has completed.\n * \n * @param {Message} message - The message to keep a receipt of its eventual transmission \n */\n addMessage(message) {\n if (this.receipts.has(message.id)) {\n throw new Error(`Duplicate transmission receipt ${JSON.stringify(message)}`);\n }\n this.ref();\n return new Promise((resolve, reject) => {\n this.receipts.set(message.id, { message, resolve, reject });\n }).catch((error) =>\n {\n if (!this.silentFailMessage)\n {\n throw error;\n }\n });\n }\n\n /**\n * Analogous to timer/socket/server#ref. Calling ref 1 or more times means\n * the event loop will not exit until this is `unref`d.\n */\n ref() {\n if (this.refTimer) return;\n this.refTimer = setTimeout( () => {\n this.refTimer = null;\n this.ref();\n }, ONE_DAY_MS);\n }\n\n /**\n * Analogous to timer/socket/server#unref. Calling unref 1 or more times means\n * the event loop will not be prevented from exiting by this message-ledger.\n */\n unref() {\n clearTimeout(this.refTimer);\n this.refTimer = null;\n }\n\n /**\n * Add a batch to the ledger\n * @param {BatchMessage} batch A batch message which we are trying to send.\n */\n addBatch(batch) {\n this.batches.set(batch.id, batch);\n }\n\n /**\n * Remove a batch from the ledger now that its messages have been received.\n * Also mark the messages as sent.\n * @param {object} ack ack message \n */\n handleAck(ack) {\n const id = ack.messageId;\n if (this.receipts.has(id)) {\n this.handleReceiptAck(id, ack);\n } else if (this.batches.has(id)) {\n this.handleBatchAck(id, ack);\n } else {\n /* Do nothing, we don't mind spurious acks */\n debugging('message-ledger') && console.debug(this.debugLabel, 'received unhandled ack', ack.toJSON());\n }\n }\n\n /**\n * Find message in receipts and mark it as sent, also resolve/delete if its a response.\n * !Note: This is untested code since current implementation always batches before sending.\n * @param {string} id id of message that we have a receipt for\n * @param {object} ack ack message\n */\n handleReceiptAck(id, ack) {\n const receipt = this.receipts.get(id);\n if (receipt.message.body.ackToken === ack.token) {\n this.fulfillResponsePromise(receipt.message);\n /* We don't delete a receipt until the promise can be fulfilled. */\n }\n }\n\n /**\n * Iterate over messages in batch and mark each as sent.\n * @param {string} id id of batch that we have sent\n * @param {object} ack ack message\n */\n handleBatchAck(id, ack) {\n const batch = this.batches.get(id);\n if (batch.ackToken === ack.token) {\n debugging('message-ledger') && console.debug(this.debugLabel, 'received batch ack');\n batch.messages.forEach( (m) => this.fulfillResponsePromise(m) );\n this.connection.sender.clearFlightDeck(batch, ack.nonce);\n this.batches.delete(id);\n } else {\n // we just ignore spurious acks\n debugging('message-ledger') && console.debug(this.debugLabel, 'received batch ack with wrong token', batch.ackToken, ack);\n }\n }\n /**\n * Like fulfillMessagePromise but ensures only responses are fulfilled.\n * @param {Connection.Message} message The response that we are fulfilling.\n */\n fulfillResponsePromise(message) {\n if (message instanceof this.connection.Response) {\n this.fulfillMessagePromise(message.id, true)\n }\n }\n\n /**\n * This method resolves\n * \n * @param {Message.id|string} id \n * @param {*|Error} value - Error instance to reject, resolves with value otherwise\n */\n fulfillMessagePromise(id, value) {\n if (this.receipts.has(id)) {\n let receipt = this.receipts.get(id);\n if (value instanceof Error) {\n receipt.reject(value);\n } else {\n receipt.resolve(value);\n }\n\n this.receipts.delete(id);\n if (this.receipts.size === 0) this.unref();\n } else {\n /* Do nothing, we don't mind duplicate responses */\n debugging('message-ledger') && console.debug(this.debugLabel, 'received duplicate response', id);\n }\n }\n\n /**\n * Fail all pending transmission receipts with err.\n * \n * @param {Error} err \n */\n failAllTransmissions(err) {\n if (!(err instanceof Error)) {\n throw new Error(\"MessageLedger.failAllTransmissions: Argument must be instance of Error\");\n }\n \n for (let id of this.receipts.keys()) {\n this.fulfillMessagePromise(id, err);\n }\n }\n}\n\nObject.assign(module.exports, {\n MessageLedger,\n});\n\n\n//# sourceURL=webpack:///./src/protocol-v4/connection/message-ledger.js?");
5563
+ eval("/**\n * @file protocol/connection/message-ledger.js\n * @author Ryan Rossiter, ryan@kingsds.network\n * @date January 2020\n *\n * The MessageLedger is responsible for storing receipts for messages\n * that are given to the sender. The receipts are used to resolve the\n * promise that was returned from the sender, to indicate when the message\n * is complete.\n * \n * Request receipt promises are resolved once a response has been received.\n * Response receipt promises are resolved once the response has been sent.\n */\nconst debugging = __webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope('dcp');\nconst ONE_DAY_MS = 24 * 60 * 60 * 1000;\nclass MessageLedger {\n constructor(connection) {\n this.connection = connection;\n const id = this.connection._id;\n this.debugLabel = connection.isInitiator? `message-ledger(i#${id}):` : `message-ledger(t#${id}):`;\n this.receipts = new Map(); // id => {message, resolve, reject\n this.batches = new Map(); // id -> [...messages]\n this.silentFailMessage = false;\n }\n\n /**\n * This method is invoked by the sender upon sending a new request.\n * It stores a receipt of the message and returns a promise that will\n * be resolved when the message's lifecycle has completed.\n * \n * @param {Message} message - The message to keep a receipt of its eventual transmission \n */\n addMessage(message) {\n if (this.receipts.has(message.id)) {\n throw new Error(`Duplicate transmission receipt ${JSON.stringify(message)}`);\n }\n this.ref();\n return new Promise((resolve, reject) => {\n this.receipts.set(message.id, { message, resolve, reject });\n }).catch((error) =>\n {\n if (!this.silentFailMessage)\n {\n throw error;\n }\n\n // If we \"silently fail\", we still need to resolve with the right-shaped\n // return value or things will break in hard-to-debug ways.\n return {\n success: false,\n payload: error,\n }\n });\n }\n\n /**\n * Analogous to timer/socket/server#ref. Calling ref 1 or more times means\n * the event loop will not exit until this is `unref`d.\n */\n ref() {\n if (this.refTimer) return;\n this.refTimer = setTimeout( () => {\n this.refTimer = null;\n this.ref();\n }, ONE_DAY_MS);\n }\n\n /**\n * Analogous to timer/socket/server#unref. Calling unref 1 or more times means\n * the event loop will not be prevented from exiting by this message-ledger.\n */\n unref() {\n clearTimeout(this.refTimer);\n this.refTimer = null;\n }\n\n /**\n * Add a batch to the ledger\n * @param {BatchMessage} batch A batch message which we are trying to send.\n */\n addBatch(batch) {\n this.batches.set(batch.id, batch);\n }\n\n /**\n * Remove a batch from the ledger now that its messages have been received.\n * Also mark the messages as sent.\n * @param {object} ack ack message \n */\n handleAck(ack) {\n const id = ack.messageId;\n if (this.receipts.has(id)) {\n this.handleReceiptAck(id, ack);\n } else if (this.batches.has(id)) {\n this.handleBatchAck(id, ack);\n } else {\n /* Do nothing, we don't mind spurious acks */\n debugging('message-ledger') && console.debug(this.debugLabel, 'received unhandled ack', ack.toJSON());\n }\n }\n\n /**\n * Find message in receipts and mark it as sent, also resolve/delete if its a response.\n * !Note: This is untested code since current implementation always batches before sending.\n * @param {string} id id of message that we have a receipt for\n * @param {object} ack ack message\n */\n handleReceiptAck(id, ack) {\n const receipt = this.receipts.get(id);\n if (receipt.message.body.ackToken === ack.token) {\n this.fulfillResponsePromise(receipt.message);\n /* We don't delete a receipt until the promise can be fulfilled. */\n }\n }\n\n /**\n * Iterate over messages in batch and mark each as sent.\n * @param {string} id id of batch that we have sent\n * @param {object} ack ack message\n */\n handleBatchAck(id, ack) {\n const batch = this.batches.get(id);\n if (batch.ackToken === ack.token) {\n debugging('message-ledger') && console.debug(this.debugLabel, 'received batch ack');\n batch.messages.forEach( (m) => this.fulfillResponsePromise(m) );\n this.connection.sender.clearFlightDeck(batch, ack.nonce);\n this.batches.delete(id);\n } else {\n // we just ignore spurious acks\n debugging('message-ledger') && console.debug(this.debugLabel, 'received batch ack with wrong token', batch.ackToken, ack);\n }\n }\n /**\n * Like fulfillMessagePromise but ensures only responses are fulfilled.\n * @param {Connection.Message} message The response that we are fulfilling.\n */\n fulfillResponsePromise(message) {\n if (message instanceof this.connection.Response) {\n this.fulfillMessagePromise(message.id, true)\n }\n }\n\n /**\n * This method resolves\n * \n * @param {Message.id|string} id \n * @param {*|Error} value - Error instance to reject, resolves with value otherwise\n */\n fulfillMessagePromise(id, value) {\n if (this.receipts.has(id)) {\n let receipt = this.receipts.get(id);\n if (value instanceof Error) {\n receipt.reject(value);\n } else {\n receipt.resolve(value);\n }\n\n this.receipts.delete(id);\n if (this.receipts.size === 0) this.unref();\n } else {\n /* Do nothing, we don't mind duplicate responses */\n debugging('message-ledger') && console.debug(this.debugLabel, 'received duplicate response', id);\n }\n }\n\n /**\n * Fail all pending transmission receipts with err.\n * \n * @param {Error} err \n */\n failAllTransmissions(err) {\n if (!(err instanceof Error)) {\n throw new Error(\"MessageLedger.failAllTransmissions: Argument must be instance of Error\");\n }\n \n for (let id of this.receipts.keys()) {\n this.fulfillMessagePromise(id, err);\n }\n }\n}\n\nObject.assign(module.exports, {\n MessageLedger,\n});\n\n\n//# sourceURL=webpack:///./src/protocol-v4/connection/message-ledger.js?");
5564
5564
 
5565
5565
  /***/ }),
5566
5566
 
@@ -5681,7 +5681,7 @@ eval("/**\n * @file protocol/transport/index.js\n * @author Ryan Ros
5681
5681
  /*! no static exports found */
5682
5682
  /***/ (function(module, exports, __webpack_require__) {
5683
5683
 
5684
- eval("/**\n * @file protocol/transport/websocket.js\n * @author Ryan Rossiter, ryan@kingsds.network\n * @date January 2020\n *\n * This module implements the SocketIO Transport that is\n * used by the protocol to connect and communicate with peers using\n * SocketIO connections.\n * \n * We listen to some socketio events for our own debugging but mostly use\n * our own mechanisms for detecting a bad transport and retrying. These are\n * independent of SocketIO's efforts to reconnect, find failures and so on.\n */\n\nconst debugging = __webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope('dcp');\nconst { leafMerge } = __webpack_require__(/*! dcp/utils/obj-merge */ \"./src/utils/obj-merge.js\");\nconst socketio = __webpack_require__(/*! socket.io-client */ \"./node_modules/socket.io-client/build/index.js\");\nconst { Transport } = __webpack_require__(/*! . */ \"./src/protocol-v4/transport/index.js\");\nvar failToConnectTimeout;\nvar failToReconnectTimeout;\n\n// How should we retry sending messages over a socket? \n// (this ignore socketio's own efforts to maintain its own connections)\n// Times in seconds to match dcpConfig standard\nconst DEFAULT_EBO_CONFIG = { \n slot: 10,\n max: 600,\n};\n\nclass SocketIOTransport extends Transport {\n /**\n * \n * @param {Socket} [socket] Target has a socket in hand, so we use it directly, \n * otherwise initiator is excepected to call `connect` when ready.\n * @param {object} [connectionOptions] A protocol-v4 connectionOptions object, built from a\n * combination of defaults and various dcpConfig components.\n * Socket.io-specific settings are in the socketio property; \n * generic properties may also be used by this library.\n */\n constructor(socket, connectionOptions={}) {\n super(\"Protocol SocketIO Transport\");\n this.socket = socket;\n // helps targets figure out which transport they are working with;\n this.name = 'socketio';\n this.shouldClose = false;\n\n\n if (this.socket) {\n this.debugLabel = 'socketio(t):';\n this.socket.debugInfo = { target: true };\n if (this.socket.handshake && this.socket.handshake.url)\n this.socket.debugInfo.urlLabel = this.socket.handshake.url;\n else\n this.socket.debugInfo.urlLabel = '<target>-' + this.socket.id;\n this.init();\n } else {\n this.debugLabel = 'socketio(i):';\n }\n\n if (connectionOptions.id)\n this.debugLabel = this.debugLabel.replace('):', `#${connectionOptions.id}):`);\n\n let config = leafMerge(\n { ebo: DEFAULT_EBO_CONFIG },\n connectionOptions.socketio\n )\n\n super.initConfig(config);\n }\n\n /**\n * Connect socket's message event to our own event emitter. Listen to other socket.io \n * events for debugging.\n */\n init() {\n debugging('socketio') && console.debug(this.debugLabel, `initializing socketio connection for ${this.socket.debugInfo.urlLabel}`);\n \n this.socket.on('message', (msg) => {\n this.emit('message', msg);\n });\n \n // be noisy about internal efforts to reconnect, but don't do anything about them\n this.socket.on('disconnect', (reason) => {\n const maxReconnectWait = this.eboConfig.max * 800;\n\n debugging('socketio') && console.debug(this.debugLabel, `disconnected from ${this.socket.debugInfo.urlLabel}; reason=${reason}`);\n if (reason === 'io server disconnect') {\n // the disconnection was initiated by the server, you need to reconnect manually\n this.socket.connect();\n }\n // else the socket will automatically try to reconnect\n\n if (!this.shouldClose) {\n var finished = false;\n \n debugging('socketio') && console.debug(this.debugLabel, `waiting up to ${maxReconnectWait/1000}s for reconnection...`);\n /**\n * Hold the nodejs process open for as long as socket.io will keep\n * trying to reconnect. Without it, the process exits before reconnect\n * can happen if all open connections to the server have died\n * unexpectedly (e.g. in a full platform restart).\n */\n failToReconnectTimeout = setTimeout(() => {\n if (finished)\n return;\n finished = true;\n this.emit('connect-failed');\n debugging('socketio') && console.debug(this.debugLabel, `failed to reconnect to ${this.socket.debugInfo.urlLabel}`);\n }, maxReconnectWait);\n\n this.socket.once('connect', () => {\n if (finished)\n return;\n finished = true;\n clearTimeout(failToReconnectTimeout);\n debugging('socketio') && console.debug(this.debugLabel, `reconnected to ${this.socket.debugInfo.urlLabel}`);\n this.emit('reconnect');\n });\n }\n });\n this.socket.on('connect_error', () => {\n clearTimeout(failToConnectTimeout);\n clearTimeout(failToReconnectTimeout);\n this.emit('connect-failed');\n });\n }\n\n /** \n * Initiate a connection with socket.io to the given target.\n * @param {object} url The URL of the target\n * @param {object} connectionOptions\n * @returns {number} time until connection should consider that this connect attempt has failed.\n */\n connect(url, connectionOptions={}) {\n var finished = false;\n\n debugging('socketio') && console.debug(this.debugLabel, 'connecting to', url && url.href);\n this.validateConnectionAttempt(url);\n\n // only autoUnref if it is possible/sane to do so (ie, in nodejs):\n const autoUnref = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\").platform === 'nodejs';\n this.socket = socketio(url.origin, { path: url.pathname, autoUnref });\n this.socket.debugInfo = { urlLabel: url.href };\n this.init();\n\n const maxConnectWait = this.eboConfig.max * 1000;\n failToConnectTimeout = setTimeout(() => {\n if (finished)\n return;\n finished = true;\n this.emit('connect-failed');\n }, maxConnectWait);\n \n this.socket.once('connect', () => {\n if (finished)\n return;\n finished = true;\n clearTimeout(failToConnectTimeout);\n debugging('socketio') && console.debug(this.debugLabel, 'connected to', url.href);\n this.emit('connected');\n });\n\n return maxConnectWait + 100; // give transport a chance to fail itself\n }\n\n async close() {\n if (!this.socket) {\n throw new Error(\"SocketIOTransport.close: Cannot close socket that was not connected\");\n }\n\n this.shouldClose = true;\n this.socket.disconnect();\n\n // No need to reconnect since we are closing the connection.\n clearTimeout(failToReconnectTimeout);\n\n if (!this.socket.disconnected) {\n throw new Error(\"Failed to close SocketIOTransport\");\n }\n }\n\n send(message) {\n debugging('socketio') && console.debug(this.debugLabel, 'sending message', message);\n\n if (!this.socket) {\n throw new Error(\"SocketIOTransport.send: Not connected\");\n }\n \n // add special logic for detecting a special message which cannot be sent.\n if (this.debugMode && message.includes(\"__debug__: disconnect\")) {\n if (!this.debugCount) this.debugCount = 1;\n this.debugCount++;\n return;\n }\n\n this.socket.send(message);\n }\n\n validateConnectionAttempt(url) {\n if (this.socket) {\n if (this.socket.connected) {\n debugging('socketio') && console.debug(this.debugLabel, 'already connected');\n throw new Error(\"SocketIOTransport.connect: Already connected\");\n } else {\n debugging('socketio') && console.debug(this.debugLabel, 'already connected but closed');\n throw new Error(\"SocketIOTransport.connect: Socket was connected and closed\");\n }\n } else {\n if (!url)\n throw new Error(\"SocketIOTransport.connect: URL must be provided\");\n }\n }\n}\n\nexports.TransportClass = SocketIOTransport;\n\n\n//# sourceURL=webpack:///./src/protocol-v4/transport/socketio.js?");
5684
+ eval("/**\n * @file protocol/transport/websocket.js\n * @author Ryan Rossiter, ryan@kingsds.network\n * @date January 2020\n *\n * This module implements the SocketIO Transport that is\n * used by the protocol to connect and communicate with peers using\n * SocketIO connections.\n * \n * We listen to some socketio events for our own debugging but mostly use\n * our own mechanisms for detecting a bad transport and retrying. These are\n * independent of SocketIO's efforts to reconnect, find failures and so on.\n */\n\nconst debugging = __webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope('dcp');\nconst { leafMerge } = __webpack_require__(/*! dcp/utils/obj-merge */ \"./src/utils/obj-merge.js\");\nconst socketio = __webpack_require__(/*! socket.io-client */ \"./node_modules/socket.io-client/build/index.js\");\nconst { Transport } = __webpack_require__(/*! . */ \"./src/protocol-v4/transport/index.js\");\nvar failToConnectTimeout;\nvar failToReconnectTimeout;\n\n// How should we retry sending messages over a socket? \n// (this ignore socketio's own efforts to maintain its own connections)\n// Times in seconds to match dcpConfig standard\nconst DEFAULT_EBO_CONFIG = { \n slot: 10,\n max: 600,\n};\n\nclass SocketIOTransport extends Transport {\n /**\n * \n * @param {Socket} [socket] Target has a socket in hand, so we use it directly, \n * otherwise initiator is excepected to call `connect` when ready.\n * @param {object} [connectionOptions] A protocol-v4 connectionOptions object, built from a\n * combination of defaults and various dcpConfig components.\n * Socket.io-specific settings are in the socketio property; \n * generic properties may also be used by this library.\n */\n constructor(socket, connectionOptions={}) {\n super(\"Protocol SocketIO Transport\");\n this.socket = socket;\n // helps targets figure out which transport they are working with;\n this.name = 'socketio';\n this.shouldClose = false;\n\n\n if (this.socket) {\n this.debugLabel = 'socketio(t):';\n this.socket.debugInfo = { target: true };\n if (this.socket.handshake && this.socket.handshake.url)\n this.socket.debugInfo.urlLabel = this.socket.handshake.url;\n else\n this.socket.debugInfo.urlLabel = '<target>-' + this.socket.id;\n this.init();\n } else {\n this.debugLabel = 'socketio(i):';\n }\n\n if (connectionOptions.id)\n this.debugLabel = this.debugLabel.replace('):', `#${connectionOptions.id}):`);\n\n let config = leafMerge(\n { ebo: DEFAULT_EBO_CONFIG },\n connectionOptions.socketio\n )\n\n super.initConfig(config);\n }\n\n /**\n * Connect socket's message event to our own event emitter. Listen to other socket.io \n * events for debugging.\n */\n init() {\n debugging('socketio') && console.debug(this.debugLabel, `initializing socketio connection for ${this.socket.debugInfo.urlLabel}`);\n \n this.socket.on('message', (msg) => {\n this.emit('message', msg);\n });\n \n // be noisy about internal efforts to reconnect, but don't do anything about them\n this.socket.on('disconnect', (reason) => {\n const maxReconnectWait = this.eboConfig.max * 800;\n\n debugging('socketio') && console.debug(this.debugLabel, `disconnected from ${this.socket.debugInfo.urlLabel}; reason=${reason}`);\n if (reason === 'io server disconnect') {\n // the disconnection was initiated by the server, you need to reconnect manually\n this.socket.connect();\n }\n // else the socket will automatically try to reconnect\n\n if (!this.shouldClose) {\n var finished = false;\n \n debugging('socketio') && console.debug(this.debugLabel, `waiting up to ${maxReconnectWait/1000}s for reconnection...`);\n /**\n * Hold the nodejs process open for as long as socket.io will keep\n * trying to reconnect. Without it, the process exits before reconnect\n * can happen if all open connections to the server have died\n * unexpectedly (e.g. in a full platform restart).\n */\n failToReconnectTimeout = setTimeout(() => {\n if (finished)\n return;\n finished = true;\n this.emit('connect-failed');\n debugging('socketio') && console.debug(this.debugLabel, `failed to reconnect to ${this.socket.debugInfo.urlLabel}`);\n }, maxReconnectWait);\n\n this.socket.once('connect', () => {\n if (finished)\n return;\n finished = true;\n clearTimeout(failToReconnectTimeout);\n debugging('socketio') && console.debug(this.debugLabel, `reconnected to ${this.socket.debugInfo.urlLabel}`);\n this.emit('reconnect');\n });\n }\n });\n this.socket.on('connect_error', () => {\n clearTimeout(failToConnectTimeout);\n clearTimeout(failToReconnectTimeout);\n this.emit('connect-failed');\n });\n }\n\n /** \n * Initiate a connection with socket.io to the given target.\n * @param {object} url The URL of the target\n * @param {object} connectionOptions\n * @returns {number} time until connection should consider that this connect attempt has failed.\n */\n connect(url, connectionOptions={}) {\n var finished = false;\n\n if (this.shouldClose)\n throw new Error('Cannot connect to SocketIOTransport instance which has already closed.');\n\n debugging('socketio') && console.debug(this.debugLabel, 'connecting to', url && url.href);\n this.validateConnectionAttempt(url);\n\n // only autoUnref if it is possible/sane to do so (ie, in nodejs):\n const autoUnref = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\").platform === 'nodejs';\n this.socket = socketio(url.origin, { path: url.pathname, autoUnref });\n this.socket.debugInfo = { urlLabel: url.href };\n this.init();\n\n const maxConnectWait = this.eboConfig.max * 1000;\n failToConnectTimeout = setTimeout(() => {\n if (finished)\n return;\n finished = true;\n this.emit('connect-failed');\n }, maxConnectWait);\n \n this.socket.once('connect', () => {\n if (finished)\n return;\n finished = true;\n clearTimeout(failToConnectTimeout);\n debugging('socketio') && console.debug(this.debugLabel, 'connected to', url.href);\n this.emit('connected');\n });\n\n return maxConnectWait + 100; // give transport a chance to fail itself\n }\n\n /** \n * Close the current instance of Socket. First we adjust the instance so that a subsequent \n * use of `send` or `open` on this instance will fast-fail, even if we throw during close.\n * Then we clean up our data structures as needed.\n */\n close()\n {\n let socket = this.socket;\n \n this.socket = false;\n this.shouldClose = true; /* Mark this so that we don't try to reanimate the corpse */\n clearTimeout(failToReconnectTimeout);\n \n if (!socket) {\n debugging() && console.debug('closing socketio transport instance that was never used');\n return;\n }\n\n socket.disconnect();\n if (!socket.disconnected) {\n throw new Error(\"Failed to close SocketIOTransport\");\n }\n }\n\n send(message) {\n debugging('socketio') && console.debug(this.debugLabel, 'sending message', message);\n\n if (!this.socket) {\n throw new Error(\"SocketIOTransport.send: Not connected\");\n }\n \n // add special logic for detecting a special message which cannot be sent.\n if (this.debugMode && message.includes(\"__debug__: disconnect\")) {\n if (!this.debugCount) this.debugCount = 1;\n this.debugCount++;\n return;\n }\n\n this.socket.send(message);\n }\n\n validateConnectionAttempt(url) {\n if (this.socket) {\n if (this.socket.connected) {\n debugging('socketio') && console.debug(this.debugLabel, 'already connected');\n throw new Error(\"SocketIOTransport.connect: Already connected\");\n } else {\n debugging('socketio') && console.debug(this.debugLabel, 'already connected but closed');\n throw new Error(\"SocketIOTransport.connect: Socket was connected and closed\");\n }\n } else {\n if (!url)\n throw new Error(\"SocketIOTransport.connect: URL must be provided\");\n }\n }\n}\n\nexports.TransportClass = SocketIOTransport;\n\n\n//# sourceURL=webpack:///./src/protocol-v4/transport/socketio.js?");
5685
5685
 
5686
5686
  /***/ }),
5687
5687