diff --git a/dist/dweb-transports-bundle.js b/dist/dweb-transports-bundle.js index 6855882..b9a3677 100644 --- a/dist/dweb-transports-bundle.js +++ b/dist/dweb-transports-bundle.js @@ -755,7 +755,7 @@ eval("/*\nThis Transport layers builds on the YJS DB and uses IPFS as its transp /*! no static exports found */ /***/ (function(module, exports, __webpack_require__) { -eval("const Url = __webpack_require__(/*! url */ \"../../../../usr/local/lib/node_modules/webpack/node_modules/url/url.js\");\nconst errors = __webpack_require__(/*! ./Errors */ \"./Errors.js\");\nconst utils = __webpack_require__(/*! ./utils */ \"./utils.js\");\n\n\n\nclass Transports {\n /*\n Handles multiple transports, API should be (almost) the same as for an individual transport)\n\n Fields:\n _transports List of transports loaded (internal)\n namingcb If set will be called cb(urls) => urls to convert to urls from names.\n _transportclasses All classes whose code is loaded e.g. {HTTP: TransportHTTP, IPFS: TransportIPFS}\n _optionspaused Saves paused option for setup\n */\n\n constructor(options, verbose) {\n if (verbose) console.log(\"Transports(%o)\",options);\n }\n\n static _connected() {\n /*\n Get an array of transports that are connected, i.e. currently usable\n */\n return this._transports.filter((t) => (!t.status));\n }\n static async p_connectedNames() {\n /*\n resolves to: an array of the names of connected transports\n */\n return this._connected().map(t => t.name);\n }\n static async p_connectedNamesParm() { // Doesnt strictly need to be async, but for consistency with Proxy it has to be.\n return (await this.p_connectedNames()).map(n => \"transport=\"+n).join('&')\n }\n static async p_statuses() {\n /*\n resolves to: a dictionary of statuses of transports, e.g. { TransportHTTP: STATUS_CONNECTED }\n */\n return this._transports.map((t) => { return {\"name\": t.name, \"status\": t.status}})\n }\n static validFor(urls, func, options) {\n /*\n Finds an array or Transports that can support this URL.\n\n Excludes any transports whose status != 0 as they aren't connected\n\n urls: Array of urls\n func: Function to check support for: fetch, store, add, list, listmonitor, reverse - see supportFunctions on each Transport class\n options: For future use\n returns: Array of pairs of url & transport instance [ [ u1, t1], [u1, t2], [u2, t1]]\n throws: CodingError if urls empty or [undefined...]\n */\n console.assert((urls && urls[0]) || [\"store\", \"newlisturls\", \"newdatabase\", \"newtable\"].includes(func), \"Coding Error: Transports.validFor called with invalid arguments: urls=\", urls, \"func=\", func); // FOr debugging old calling patterns with [ undefined ]\n if (!(urls && urls.length > 0)) {\n return this._connected().filter((t) => (t.supports(undefined, func)))\n .map((t) => [undefined, t]);\n } else {\n return [].concat(\n ...urls.map((url) => typeof url === 'string' ? Url.parse(url) : url) // parse URLs once\n .map((url) =>\n this._connected().filter((t) => (t.supports(url, func))) // [ t1, t2 ]\n .map((t) => [url, t]))); // [[ u, t1], [u, t2]]\n }\n }\n static async p_urlsValidFor(urls, func, options) {\n // Need a async version of this for serviceworker and TransportsProxy\n return this.validFor(urls, func, options).map((ut) => ut[0]);\n }\n static http(verbose) {\n // Find an http transport if it exists, so for example YJS can use it.\n return Transports._connected().find((t) => t.name === \"HTTP\")\n }\n static ipfs(verbose) {\n // Find an ipfs transport if it exists, so for example YJS can use it.\n return Transports._connected().find((t) => t.name === \"IPFS\")\n }\n\n static webtorrent(verbose) {\n // Find an ipfs transport if it exists, so for example ServiceWorker.p_respondWebTorrent can use it.\n return Transports._connected().find((t) => t.name === \"WEBTORRENT\")\n }\n\n static async p_resolveNames(urls) {\n /* If and only if TransportNAME was loaded (it might not be as it depends on higher level classes like Domain and SmartDict)\n then resolve urls that might be names, returning a modified array.\n */\n if (this.namingcb) {\n return await this.namingcb(urls); // Array of resolved urls\n } else {\n return urls;\n }\n }\n static resolveNamesWith(cb) {\n // Set a callback for p_resolveNames\n this.namingcb = cb;\n }\n\n static async _p_rawstore(tt, data, {verbose}) {\n // Internal method to store at known transports\n let errs = [];\n let rr = await Promise.all(tt.map(async function(t) {\n try {\n return await t.p_rawstore(data, {verbose}); //url\n } catch(err) {\n console.log(\"Could not rawstore to\", t.name, err.message);\n errs.push(err);\n return undefined;\n }\n }));\n rr = rr.filter((r) => !!r); // Trim any that had errors\n if (!rr.length) {\n throw new errors.TransportError(errs.map((err)=>err.message).join(', ')); // New error with concatenated messages\n }\n return rr;\n\n }\n static async p_rawstore(data, {verbose}) {\n /*\n data: Raw data to store - typically a string, but its passed on unmodified here\n returns: Array of urls of where stored\n throws: TransportError with message being concatenated messages of transports if NONE of them succeed.\n */\n let tt = this.validFor(undefined, \"store\").map(([u, t]) => t); // Valid connected transports that support \"store\"\n if (verbose) console.log(\"Valid for transports:\", tt.map(t => t.name));\n if (!tt.length) {\n throw new errors.TransportError('Transports.p_rawstore: Cant find transport for store');\n }\n return this._p_rawstore(tt, data, {verbose});\n }\n static async p_rawlist(urls, {verbose=false}={}) {\n urls = await this.p_resolveNames(urls); // If naming is loaded then convert to a name\n let tt = this.validFor(urls, \"list\"); // Valid connected transports that support \"store\"\n if (!tt.length) {\n throw new errors.TransportError('Transports.p_rawlist: Cant find transport for urls:'+urls.join(','));\n }\n let errs = [];\n let ttlines = await Promise.all(tt.map(async function([url, t]) {\n try {\n return await t.p_rawlist(url, {verbose}); // [sig]\n } catch(err) {\n console.log(\"Could not rawlist \", url, \"from\", t.name, err.message);\n errs.push(err);\n return [];\n }\n })); // [[sig,sig],[sig,sig]]\n if (errs.length >= tt.length) {\n // All Transports failed (maybe only 1)\n throw new errors.TransportError(errs.map((err)=>err.message).join(', ')); // New error with concatenated messages\n }\n let uniques = {}; // Used to filter duplicates\n return [].concat(...ttlines)\n .filter((x) => (!uniques[x.signature] && (uniques[x.signature] = true)));\n }\n\n static async p_rawfetch(urls, opts) {\n /*\n Fetch the data for a url, transports act on the data, typically storing it.\n urls:\tarray of urls to retrieve (any are valid)\n opts {\n verbose,\n start, integer - first byte wanted\n end integer - last byte wanted (note this is inclusive start=0,end=1023 is 1024 bytes\n timeoutMS integer - max time to wait on transports (IPFS) that support it\n }\n returns:\tstring - arbitrary bytes retrieved.\n throws: TransportError with concatenated error messages if none succeed.\n throws: CodingError if urls empty or [undefined ... ]\n */\n let verbose = opts.verbose;\n urls = await this.p_resolveNames(urls); // If naming is loaded then convert to a name\n let tt = this.validFor(urls, \"fetch\"); //[ [Url,t],[Url,t]] throws CodingError on empty /undefined urls\n if (!tt.length) {\n throw new errors.TransportError(\"Transports.p_fetch cant find any transport for urls: \" + urls);\n }\n //With multiple transports, it should return when the first one returns something.\n let errs = [];\n let failedtransports = []; // Will accumulate any transports fail on before the success\n for (const [url, t] of tt) {\n try {\n let data = await t.p_rawfetch(url, opts); // throws errors if fails or timesout\n //TODO-MULTI-GATEWAY working here - it doesnt quite work yet as the \"Add\" on browser gets different url than on server\n if (opts.relay && failedtransports.length) {\n console.log(`Relaying ${data.length} bytes from ${typeof url === \"string\" ? url : url.href} to ${failedtransports.map(t=>t.name)}`);\n this._p_rawstore(failedtransports, data, {verbose})\n .then(uu => console.log(`Relayed to ${uu}`)); // Happening async, not waiting and dont care if fails\n }\n //END TODO-MULTI-GATEWAY\n return data;\n } catch (err) {\n failedtransports.push(t);\n errs.push(err);\n console.log(\"Could not retrieve \", url && url.href, \"from\", t && t.name, err.message);\n // Don't throw anything here, loop round for next, only throw if drop out bottom\n //TODO-MULTI-GATEWAY potentially copy from success to failed URLs.\n }\n }\n throw new errors.TransportError(errs.map((err)=>err.message).join(', ')); //Throw err with combined messages if none succeed\n }\n\n static async p_rawadd(urls, sig, {verbose=false}={}) {\n /*\n urls: of lists to add to\n sig: Sig to add\n returns: undefined\n throws: TransportError with message being concatenated messages of transports if NONE of them succeed.\n */\n //TODO-MULTI-GATEWAY might be smarter about not waiting but Promise.race is inappropriate as returns after a failure as well.\n urls = await this.p_resolveNames(urls); // If naming is loaded then convert to a name\n let tt = this.validFor(urls, \"add\"); // Valid connected transports that support \"store\"\n if (!tt.length) {\n throw new errors.TransportError('Transports.p_rawstore: Cant find transport for urls:'+urls.join(','));\n }\n let errs = [];\n await Promise.all(tt.map(async function([u, t]) {\n try {\n await t.p_rawadd(u, sig, {verbose}); //undefined\n return undefined;\n } catch(err) {\n console.log(\"Could not rawlist \", u, \"from\", t.name, err.message);\n errs.push(err);\n return undefined;\n }\n }));\n if (errs.length >= tt.length) {\n // All Transports failed (maybe only 1)\n throw new errors.TransportError(errs.map((err)=>err.message).join(', ')); // New error with concatenated messages\n }\n return undefined;\n\n }\n\n static listmonitor(urls, cb, opts={}) {\n /*\n Add a listmonitor for each transport - note this means if multiple transports support it, then will get duplicate events back if everyone else is notifying all of them.\n */\n // Note cant do p_resolveNames since sync but should know real urls of resource by here.\n this.validFor(urls, \"listmonitor\")\n .map(([u, t]) => t.listmonitor(u, cb, opts));\n }\n\n static async p_newlisturls(cl, {verbose=false}={}) {\n // Create a new list in any transport layer that supports lists.\n // cl is a CommonList or subclass and can be used by the Transport to get info for choosing the list URL (normally it won't use it)\n // Note that normally the CL will not have been stored yet, so you can't use its urls.\n let uuu = await Promise.all(this.validFor(undefined, \"newlisturls\")\n .map(([u, t]) => t.p_newlisturls(cl, {verbose})) ); // [ [ priv, pub] [ priv, pub] [priv pub] ]\n return [uuu.map(uu=>uu[0]), uuu.map(uu=>uu[1])]; // [[ priv priv priv ] [ pub pub pub ] ]\n }\n\n // Stream handling ===========================================\n \n static async p_f_createReadStream(urls, {verbose=false, wanturl=false}={}) { // Note options is options for selecting a stream, not the start/end in a createReadStream call\n /*\n urls: Urls of the stream\n returns: f(opts) => stream returning bytes from opts.start || start of file to opts.end-1 || end of file\n */\n let tt = this.validFor(urls, \"createReadStream\", {}); //[ [Url,t],[Url,t]] // Can pass options TODO-STREAM support options in validFor\n if (!tt.length) {\n throw new errors.TransportError(\"Transports.p_createReadStream cant find any transport for urls: \" + urls);\n }\n //With multiple transports, it should return when the first one returns something.\n let errs = [];\n for (const [url, t] of tt) {\n try {\n return await t.p_f_createReadStream(url, {verbose, wanturl} );\n } catch (err) {\n errs.push(err);\n console.log(\"Could not retrieve \", url.href, \"from\", t.name, err.message);\n // Don't throw anything here, loop round for next, only throw if drop out bottom\n //TODO-MULTI-GATEWAY potentially copy from success to failed URLs.\n }\n }\n throw new errors.TransportError(errs.map((err)=>err.message).join(', ')); //Throw err with combined messages if none succeed\n}\n\n\n// KeyValue support ===========================================\n\n static async p_get(urls, keys, {verbose=false}={}) {\n /*\n Fetch the values for a url and one or more keys, transports act on the data, typically storing it.\n urls:\tarray of urls to retrieve (any are valid)\n keys: array of keys wanted or single key\n returns:\tstring - arbitrary bytes retrieved or dict of key: value\n throws: TransportError with concatenated error messages if none succeed.\n */\n let tt = this.validFor(urls, \"get\"); //[ [Url,t],[Url,t]]\n if (!tt.length) {\n throw new errors.TransportError(\"Transports.p_get cant find any transport for urls: \" + urls);\n }\n //With multiple transports, it should return when the first one returns something.\n let errs = [];\n for (const [url, t] of tt) {\n try {\n return await t.p_get(url, keys, {verbose}); //TODO-MULTI-GATEWAY potentially copy from success to failed URLs.\n } catch (err) {\n errs.push(err);\n console.log(\"Could not retrieve \", url.href, \"from\", t.name, err.message);\n // Don't throw anything here, loop round for next, only throw if drop out bottom\n }\n }\n throw new errors.TransportError(errs.map((err)=>err.message).join(', ')); //Throw err with combined messages if none succeed\n }\n static async p_set(urls, keyvalues, value, {verbose=false}={}) {\n /* Set a series of key/values or a single value\n keyvalues: Either dict or a string\n value: if kv is a string, this is the value to set\n throws: TransportError with message being concatenated messages of transports if NONE of them succeed.\n */\n urls = await this.p_resolveNames(urls); // If naming is loaded then convert to a name\n let tt = this.validFor(urls, \"set\"); //[ [Url,t],[Url,t]]\n if (!tt.length) {\n throw new errors.TransportError(\"Transports.p_set cant find any transport for urls: \" + urls);\n }\n let errs = [];\n let success = false;\n await Promise.all(tt.map(async function([url, t]) {\n try {\n await t.p_set(url, keyvalues, value, {verbose});\n success = true; // Any one success will return true\n } catch(err) {\n console.log(\"Could not rawstore to\", t.name, err.message);\n errs.push(err);\n }\n }));\n if (!success) {\n throw new errors.TransportError(errs.map((err)=>err.message).join(', ')); // New error with concatenated messages\n }\n }\n\n static async p_delete(urls, keys, {verbose=false}={}) {\n /* Delete a key or a list of keys\n kv: Either dict or a string\n value: if kv is a string, this is the value to set\n throws: TransportError with message being concatenated messages of transports if NONE of them succeed.\n */\n urls = await this.p_resolveNames(urls); // If naming is loaded then convert to a name\n let tt = this.validFor(urls, \"set\"); //[ [Url,t],[Url,t]]\n if (!tt.length) {\n throw new errors.TransportError(\"Transports.p_set cant find any transport for urls: \" + urls);\n }\n let errs = [];\n let success = false;\n await Promise.all(tt.map(async function([url, t]) {\n try {\n await t.p_delete(url, keys, {verbose});\n success = true; // Any one success will return true\n } catch(err) {\n console.log(\"Could not rawstore to\", t.name, err.message);\n errs.push(err);\n }\n }));\n if (!success) {\n throw new errors.TransportError(errs.map((err)=>err.message).join(', ')); // New error with concatenated messages\n }\n }\n static async p_keys(urls, {verbose=false}={}) {\n /*\n Fetch the values for a url and one or more keys, transports act on the data, typically storing it.\n urls:\tarray of urls to retrieve (any are valid)\n keys: array of keys wanted\n returns:\tstring - arbitrary bytes retrieved or dict of key: value\n throws: TransportError with concatenated error messages if none succeed.\n */\n urls = await this.p_resolveNames(urls); // If naming is loaded then convert to a name\n let tt = this.validFor(urls, \"keys\"); //[ [Url,t],[Url,t]]\n if (!tt.length) {\n throw new errors.TransportError(\"Transports.p_keys cant find any transport for urls: \" + urls);\n }\n //With multiple transports, it should return when the first one returns something.\n let errs = [];\n for (const [url, t] of tt) {\n try {\n return await t.p_keys(url, {verbose}); //TODO-MULTI-GATEWAY potentially copy from success to failed URLs.\n } catch (err) {\n errs.push(err);\n console.log(\"Could not retrieve keys for\", url.href, \"from\", t.name, err.message);\n // Don't throw anything here, loop round for next, only throw if drop out bottom\n }\n }\n throw new errors.TransportError(errs.map((err)=>err.message).join(', ')); //Throw err with combined messages if none succeed\n }\n\n static async p_getall(urls, {verbose=false}={}) {\n /*\n Fetch the values for a url and one or more keys, transports act on the data, typically storing it.\n urls:\tarray of urls to retrieve (any are valid)\n keys: array of keys wanted\n returns:\tarray of strings returned for the keys. //TODO consider issues around return a data type rather than array of strings\n throws: TransportError with concatenated error messages if none succeed.\n */\n urls = await this.p_resolveNames(urls); // If naming is loaded then convert to a name\n let tt = this.validFor(urls, \"getall\"); //[ [Url,t],[Url,t]]\n if (!tt.length) {\n throw new errors.TransportError(\"Transports.p_getall cant find any transport for urls: \" + urls);\n }\n //With multiple transports, it should return when the first one returns something.\n let errs = [];\n for (const [url, t] of tt) {\n try {\n return await t.p_getall(url, {verbose}); //TODO-MULTI-GATEWAY potentially copy from success to failed URLs.\n } catch (err) {\n errs.push(err);\n console.log(\"Could not retrieve all keys for\", url.href, \"from\", t.name, err.message);\n // Don't throw anything here, loop round for next, only throw if drop out bottom\n }\n }\n throw new errors.TransportError(errs.map((err)=>err.message).join(', ')); //Throw err with combined messages if none succeed\n }\n\n static async p_newdatabase(pubkey, {verbose=false}={}) {\n /*\n Create a new database in any transport layer that supports databases (key value pairs).\n pubkey: CommonList, KeyPair, or exported public key\n resolves to: [ privateurl, publicurl]\n */\n let uuu = await Promise.all(this.validFor(undefined, \"newdatabase\")\n .map(([u, t]) => t.p_newdatabase(pubkey, {verbose})) ); // [ { privateurl, publicurl} { privateurl, publicurl} { privateurl, publicurl} ]\n return { privateurls: uuu.map(uu=>uu.privateurl), publicurls: uuu.map(uu=>uu.publicurl) }; // { privateurls: [], publicurls: [] }\n }\n\n static async p_newtable(pubkey, table, {verbose=false}={}) {\n /*\n Create a new table in any transport layer that supports the function (key value pairs).\n pubkey: CommonList, KeyPair, or exported public key\n resolves to: [ privateurl, publicurl]\n */\n let uuu = await Promise.all(this.validFor(undefined, \"newtable\")\n .map(([u, t]) => t.p_newtable(pubkey, table, {verbose})) ); // [ [ priv, pub] [ priv, pub] [priv pub] ]\n return { privateurls: uuu.map(uu=>uu.privateurl), publicurls: uuu.map(uu=>uu.publicurl)}; // {privateurls: [ priv priv priv ], publicurls: [ pub pub pub ] }\n }\n\n static async p_connection(urls, verbose) {\n /*\n Do any asynchronous connection opening work prior to potentially synchronous methods (like monitor)\n */\n urls = await this.p_resolveNames(urls); // If naming is loaded then convert to a name\n await Promise.all(\n this.validFor(urls, \"connection\")\n .map(([u, t]) => t.p_connection(u, verbose)));\n }\n\n static monitor(urls, cb, verbose) {\n /*\n Add a listmonitor for each transport - note this means if multiple transports support it, then will get duplicate events back if everyone else is notifying all of them.\n Stack: KVT()|KVT.p_new => KVT.monitor => (a: Transports.monitor => YJS.monitor)(b: dispatchEvent)\n */\n //Cant' its async. urls = await this.p_resolveNames(urls); // If naming is loaded then convert to a name\n this.validFor(urls, \"monitor\")\n .map(([u, t]) => t.monitor(u, cb, verbose));\n }\n\n // Setup and connection\n\n static addtransport(t) {\n /*\n Add a transport to _transports,\n */\n Transports._transports.push(t);\n }\n\n // Setup Transports - setup0 is called once, and should return quickly, p_setup1 and p_setup2 are asynchronous and p_setup2 relies on p_setup1 having resolved.\n\n static setup0(tabbrevs, options, verbose, cb) {\n /*\n Setup Transports for a range of classes\n tabbrevs is abbreviation HTTP, IPFS, LOCAL or list of them e.g. \"HTTP,IPFS\"\n Handles \"LOCAL\" specially, turning into a HTTP to a local server (for debugging)\n\n returns array of transport instances\n */\n // \"IPFS\" or \"IPFS,LOCAL,HTTP\"\n let localoptions = {http: {urlbase: \"http://localhost:4244\"}};\n return tabbrevs.map((tabbrev) => {\n let transportclass;\n if (tabbrev === \"LOCAL\") {\n transportclass = this._transportclasses[\"HTTP\"];\n } else {\n transportclass = this._transportclasses[tabbrev];\n }\n if (!transportclass) {\n let tt = Object.keys(this._transportclasses);\n console.error(`Requested ${tabbrev} but ${tt.length ? tt : \"No\"} transports have been loaded`);\n return undefined;\n } else {\n return transportclass.setup0(tabbrev === \"LOCAL\" ? localoptions : options, verbose);\n }\n }).filter(f => !!f); // Trim out any undefined\n }\n static async p_setup1(verbose, cb) {\n /* Second stage of setup, connect if possible */\n // Does all setup1a before setup1b since 1b can rely on ones with 1a, e.g. YJS relies on IPFS\n await Promise.all(this._transports\n .filter((t) => (! this._optionspaused.includes(t.name)))\n .map((t) => t.p_setup1(verbose, cb)))\n }\n static async p_setup2(verbose, cb) {\n /* Second stage of setup, connect if possible */\n // Does all setup1a before setup1b since 1b can rely on ones with 1a, e.g. YJS relies on IPFS\n await Promise.all(this._transports\n .filter((t) => (! this._optionspaused.includes(t.name)))\n .map((t) => t.p_setup2(verbose, cb)))\n }\n\n static async refreshstatus(t) {\n //Note \"this' undefined as called as callback\n let statusclasses = [\"transportstatus0\",\"transportstatus1\",\"transportstatus2\",\"transportstatus3\",\"transportstatus4\"];\n let el = t.statuselement;\n if (el) {\n el.classList.remove(...statusclasses);\n el.classList.add(statusclasses[t.status]);\n }\n if (Transports.statuscb) {\n Transports.statuscb(t);\n }\n }\n\n static async p_connect(options, verbose) {\n /*\n This is a standardish starting process, feel free to subclass or replace !\n It will connect to a set of standard transports and is intended to work inside a browser.\n options = { defaulttransports: [\"IPFS\"], statuselement: el, http: {}, ipfs: {}; paused: [\"IPFS\"] }\n */\n if (verbose) console.group(\"p_connect ---\");\n try {\n options = options || {};\n let setupoptions = {};\n let tabbrevs = options.transports; // Array of transport abbreviations\n this._optionspaused = (options.paused || []).map(n => n.toUpperCase()); // Array of transports paused - defaults to none, upper cased\n if (!(tabbrevs && tabbrevs.length)) { tabbrevs = options.defaulttransports || [] }\n if (! tabbrevs.length) { tabbrevs = [\"HTTP\", \"YJS\", \"IPFS\", \"WEBTORRENT\"]; }\n tabbrevs = tabbrevs.map(n => n.toUpperCase());\n let transports = this.setup0(tabbrevs, options, verbose);\n if (options.statuscb) {\n this.statuscb = options.statuscb;\n }\n if (!!options.statuselement) {\n while (statuselement.lastChild) {statuselement.removeChild(statuselement.lastChild); } // Remove any exist status\n statuselement.appendChild(\n utils.createElement(\"UL\", {}, transports.map(t => {\n let el = utils.createElement(\"LI\",\n {onclick: \"this.source.togglePaused(DwebTransports.refreshstatus);\", source: t, name: t.name}, //TODO-SW figure out how t osend this back\n t.name);\n t.statuselement = el; // Save status element on transport\n return el;\n }\n )));\n }\n await this.p_setup1(verbose, this.refreshstatus);\n await this.p_setup2(verbose, this.refreshstatus);\n } catch(err) {\n console.error(\"ERROR in p_connect:\",err.message);\n throw(err);\n }\n if (verbose) console.groupEnd(\"p_connect ---\");\n }\n\n static async p_urlsFrom(url) {\n /* Utility to convert to urls form wanted for Transports functions, e.g. from user input\n url: Array of urls, or string representing url or representing array of urls\n return: Array of strings representing url\n */\n if (typeof(url) === \"string\") {\n if (url[0] === '[')\n url = JSON.parse(url);\n else if (url.includes(','))\n url = url.split(',');\n else\n url = [ url ];\n }\n if (!Array.isArray(url)) throw new Error(`Unparsable url: ${url}`);\n return url;\n }\n\n static async p_httpfetchurl(urls) {\n /*\n Utility to take a array of Transport urls, convert back to a single url that can be used for a fetch, typically\n this is done when cant handle a stream, so want to give the url to the