const Url = require('url'); const stream = require('readable-stream'); const errors = require('./Errors'); // Standard Dweb Errors function delay(ms, val) { return new Promise(resolve => {setTimeout(() => { resolve(val); },ms)})} class Transport { constructor(options, verbose) { /* Doesnt do anything, its all done by SuperClasses, Superclass should merge with default options, call super Fields: statuselement: If set is an HTML Element that should be adjusted to indicate status (this is managed by Transports, just stored on Transport) statuscb: Callback when status changes name: Short name of element e.g. HTTP IPFS WEBTORRENT */ } static setup0(options, verbose) { /* First part of setup, create obj, add to Transports but dont attempt to connect, typically called instead of p_setup if want to parallelize connections. */ throw new errors.IntentionallyUnimplementedError("Intentionally undefined function Transport.setup0 should have been subclassed"); } p_setup1(options, verbose) { /* Setup the resource and open any P2P connections etc required to be done just once. Asynchronous and should leave status=STATUS_STARTING until it resolves, or STATUS_FAILED if fails. cb (t)=>void If set, will be called back as status changes (so could be multiple times) Resolves to the Transport instance */ return this; } p_setup2(options, verbose) { /* Works like p_setup1 but runs after p_setup1 has completed for all transports. This allows for example YJS to wait for IPFS to be connected in TransportIPFS.setup1() and then connect itself using the IPFS object. cb (t)=>void If set, will be called back as status changes (so could be multiple times) Resolves to the Transport instance */ return this; } static async p_setup(options, verbose, cb) { /* A deprecated utility to simply setup0 then p_setup1 then p_setup2 to allow a transport to be started in one step, normally Transports.p_setup should be called instead. */ let t = await this.setup0(options, verbose) // Sync version that doesnt connect .p_setup1(verbose, cb); // And connect return t.p_setup2(verbose, cb); // And connect } togglePaused(cb) { //TODO-SW move to Transports > TransportsProxy > UI /* Switch the state of the transport between STATUS_CONNECTED and STATUS_PAUSED, in the paused state it will not be used for transport but, in some cases, will still do background tasks like serving files. cb(transport)=>void a callback called after this is run, may be used for example to change the UI */ switch (this.status) { case Transport.STATUS_CONNECTED: this.status = Transport.STATUS_PAUSED; break; case Transport.STATUS_PAUSED: this.status = Transport.STATUS_CONNECTED; // Superclass might change to STATUS_STARTING if needs to stop/restart break; } if (cb) cb(this); } async p_status(verbose) { /* Check the status of the underlying transport. This may update the "status" field from the underlying transport. returns: a numeric code for the status of a transport. */ return this.status; } supports(url, func) { /* Determine if this transport supports a certain set of URLs and a func :param url: String or parsed URL :return: true if this protocol supports these URLs and this func :throw: TransportError if invalid URL */ if (typeof url === "string") { url = Url.parse(url); // For efficiency, only parse once. } if (url && !url.protocol) { throw new Error("URL failed to specific a scheme (before :) " + url.href) } //Should be TransportError but out of scope here // noinspection Annotator supportURLs is defined in subclasses return ( (!url || this.supportURLs.includes(url.protocol.slice(0, -1))) && (!func || this.supportFunctions.includes(func))) } p_rawstore(data, opts) { /* Store a blob of data onto the decentralised transport. Returns a promise that resolves to the url of the data :param string|Buffer data: Data to store - no assumptions made to size or content :param boolean verbose: true for debugging output :resolve string: url of data stored */ throw new errors.ToBeImplementedError("Intentionally undefined function Transport.p_rawstore should have been subclassed"); } async p_rawstoreCaught(data, {verbose}) { try { return await this.p_rawstore(data, {verbose}); } catch (err) { } } p_store() { throw new errors.ToBeImplementedError("Undefined function Transport.p_store - may define higher level semantics here (see Python)"); } //noinspection JSUnusedLocalSymbols p_rawfetch(url, {timeoutMS=undefined, start=undefined, end=undefined, relay=false, verbose=false}={}) { /* Fetch some bytes based on a url, no assumption is made about the data in terms of size or structure. Where required by the underlying transport it should retrieve a number if its "blocks" and concatenate them. Returns a new Promise that resolves currently to a string. There may also be need for a streaming version of this call, at this point undefined. :param string url: URL of object being retrieved :param verbose: true for debugging output :param timeoutMS Max time to wait on transports that support it (IPFS for fetch) :param start,end Inclusive byte range wanted (must be supported, uses a "slice" on output if transport ignores it. :param relay If first transport fails, try and retrieve on 2nd, then store on 1st, and so on. :resolve string: Return the object being fetched, (note currently returned as a string, may refactor to return Buffer) :throws: TransportError if url invalid - note this happens immediately, not as a catch in the promise */ console.assert(false, "Intentionally undefined function Transport.p_rawfetch should have been subclassed"); } p_fetch() { throw new errors.ToBeImplementedError("Undefined function Transport.p_fetch - may define higher level semantics here (see Python)"); } p_rawadd(url, sig, {verbose=false}={}) { /* Store a new list item, ideally it should be stored so that it can be retrieved either by "signedby" (using p_rawlist) or by "url" (with p_rawreverse). The underlying transport does not need to guarantee the signature, an invalid item on a list should be rejected on higher layers. :param string url: String identifying an object being added to the list. :param Signature sig: A signature data structure. :param boolean verbose: true for debugging output :resolve undefined: */ throw new errors.ToBeImplementedError("Undefined function Transport.p_rawadd"); } p_rawlist(url, {verbose=false}={}) { /* Fetch all the objects in a list, these are identified by the url of the public key used for signing. (Note this is the 'signedby' parameter of the p_rawadd call, not the 'url' parameter Returns a promise that resolves to the list. Each item of the list is a dict: {"url": url, "date": date, "signature": signature, "signedby": signedby} List items may have other data (e.g. reference ids of underlying transport) :param string url: String with the url that identifies the list. :param boolean verbose: true for debugging output :resolve array: An array of objects as stored on the list. */ throw new errors.ToBeImplementedError("Undefined function Transport.p_rawlist"); } p_list() { throw new Error("Undefined function Transport.p_list"); } p_newlisturls(cl, {verbose=false}={}) { /* Must be implemented by any list, return a pair of URLS that may be the same, private and public links to the list. returns: ( privateurl, publicurl) e.g. yjs:xyz/abc or orbitdb:a123 */ throw new Error("undefined function Transport.p_newlisturls"); } //noinspection JSUnusedGlobalSymbols p_rawreverse(url, {verbose=false}={}) { /* Similar to p_rawlist, but return the list item of all the places where the object url has been listed. The url here corresponds to the "url" parameter of p_rawadd Returns a promise that resolves to the list. :param string url: String with the url that identifies the object put on a list. :param boolean verbose: true for debugging output :resolve array: An array of objects as stored on the list. */ throw new errors.ToBeImplementedError("Undefined function Transport.p_rawreverse"); } listmonitor(url, callback, verbose) { /* Setup a callback called whenever an item is added to a list, typically it would be called immediately after a p_rawlist to get any more items not returned by p_rawlist. :param url: string Identifier of list (as used by p_rawlist and "signedby" parameter of p_rawadd :param callback: function(obj) Callback for each new item added to the list obj is same format as p_rawlist or p_rawreverse :param verbose: boolean - true for debugging output */ console.log("Undefined function Transport.listmonitor"); // Note intentionally a log, as legitamte to not implement it } // ==== TO SUPPORT KEY VALUE INTERFACES IMPLEMENT THESE ===== // Support for Key-Value pairs as per // https://docs.google.com/document/d/1yfmLRqKPxKwB939wIy9sSaa7GKOzM5PrCZ4W1jRGW6M/edit# async p_newdatabase(pubkey, {verbose=false}={}) { /* Create a new database based on some existing object pubkey: Something that is, or has a pubkey, by default support Dweb.PublicPrivate, KeyPair or an array of strings as in the output of keypair.publicexport() returns: {publicurl, privateurl} which may be the same if there is no write authentication */ throw new errors.ToBeImplementedError("Undefined function Transport.p_newdatabase"); } //TODO maybe change the listmonitor / monitor code for to use "on" and the structure of PP.events //TODO but note https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy about Proxy which might be suitable, prob not as doesnt map well to lists async p_newtable(pubkey, table, {verbose=false}={}) { /* Create a new table, pubkey: Is or has a pubkey (see p_newdatabase) table: String representing the table - unique to the database returns: {privateurl, publicurl} which may be the same if there is no write authentication */ throw new errors.ToBeImplementedError("Undefined function Transport.p_newtable"); } async p_set(url, keyvalues, value, {verbose=false}={}) { // url = yjs:/yjs/database/table/key /* Set one or more keys in a table. url: URL of the table keyvalues: String representing a single key OR dictionary of keys value: String or other object to be stored (its not defined yet what objects should be supported, e.g. any object ? */ throw new errors.ToBeImplementedError("Undefined function Transport.p_set"); } async p_get(url, keys, {verbose=false}={}) { /* Get one or more keys from a table url: URL of the table keys: Array of keys returns: Dictionary of values found (undefined if not found) */ throw new errors.ToBeImplementedError("Undefined function Transport.p_get"); } async p_delete(url, keys, {verbose=false}={}) { /* Delete one or more keys from a table url: URL of the table keys: Array of keys */ throw new errors.ToBeImplementedError("Undefined function Transport.p_delete"); } async p_keys(url, {verbose=false}={}) { /* Return a list of keys in a table (suitable for iterating through) url: URL of the table returns: Array of strings */ throw new errors.ToBeImplementedError("Undefined function Transport.p_keys"); } async p_getall(url, {verbose=false}={}) { /* Return a dictionary representing the table url: URL of the table returns: Dictionary of Key:Value pairs, note take care if this could be large. */ throw new errors.ToBeImplementedError("Undefined function Transport.p_keys"); } static async p_f_createReadStream(url, {wanturl=false, verbose=false}) { /* Provide a function of the form needed by tag and renderMedia library etc url Urls of stream wanturl True if want the URL of the stream (for service workers) returns f(opts) => stream returning bytes from opts.start || start of file to opts.end-1 || end of file */ } // ------ UTILITY FUNCTIONS, NOT REQD TO BE SUBCLASSED ---- static mergeoptions(a) { /* Deep merge options dictionaries */ let c = {}; for (let i = 0; i < arguments.length; i++) { let b = arguments[i]; for (let key in b) { let val = b[key]; if ((typeof val === "object") && !Array.isArray(val) && c[key]) { c[key] = Transport.mergeoptions(a[key], b[key]); } else { c[key] = b[key]; } } } return c; } async p_test_kvt(urlexpectedsubstring, verbose=false) { /* Test the KeyValue functionality of any transport that supports it. urlexpectedsubstring: Some string expected in the publicurl of the table. */ if (verbose) {console.log(this.name,"p_test_kvt")} try { let table = await this.p_newtable("NACL VERIFY:1234567","mytable", {verbose}); let mapurl = table.publicurl; if (verbose) console.log("newtable=",mapurl); console.assert(mapurl.includes(urlexpectedsubstring)); await this.p_set(mapurl, "testkey", "testvalue", {verbose}); let res = await this.p_get(mapurl, "testkey", {verbose}); console.assert(res === "testvalue"); await this.p_set(mapurl, "testkey2", {foo: "bar"}, {verbose}); // Try setting to an object res = await this.p_get(mapurl, "testkey2", {verbose}); console.assert(res.foo === "bar"); await this.p_set(mapurl, "testkey3", [1,2,3], {verbose}); // Try setting to an array res = await this.p_get(mapurl, "testkey3", {verbose}); console.assert(res[1] === 2); res = await this.p_keys(mapurl, {verbose}); console.assert(res.includes("testkey") && res.includes("testkey3")); res = await this.p_delete(mapurl, ["testkey"], {verbose}); res = await this.p_getall(mapurl, {verbose}); if (verbose) console.log("getall=>",res); console.assert(res.testkey2.foo === "bar" && res.testkey3["1"] === 2 && !res.testkey1); await delay(200); if (verbose) console.log(this.name, "p_test_kvt complete") } catch(err) { console.log("Exception thrown in ", this.name, "p_test_kvt:", err.message); throw err; } } } Transport.STATUS_CONNECTED = 0; // Connected - all other numbers are some version of not ok to use Transport.STATUS_FAILED = 1; // Failed to connect Transport.STATUS_STARTING = 2; // In the process of connecting Transport.STATUS_LOADED = 3; // Code loaded, but haven't tried to connect. (this is typically hard coded in subclasses constructor) Transport.STATUS_PAUSED = 4; // It was launched, probably connected, but now paused so will be ignored by validFor // Note this is copied to dweb-archive/Nav.js so check if change exports = module.exports = Transport;