/* This Transport layers uses GUN. */ const Url = require('url'); process.env.GUN_ENV = "false"; const Gun = require('gun/gun.js'); // TODO-GUN switchback to gun/gun at some point to get minimized version require('gun/lib/path.js'); // Other Dweb modules const errors = require('./Errors'); // Standard Dweb Errors const Transport = require('./Transport.js'); // Base class for TransportXyz const Transports = require('./Transports'); // Manage all Transports that are loaded const utils = require('./utils'); // Utility functions // Utility packages (ours) And one-liners //unused currently: function delay(ms, val) { return new Promise(resolve => {setTimeout(() => { resolve(val); },ms)})} let defaultoptions = { peers: [ "https://dweb.me:4246/gun" ] //localstore: true #True is default TODO-GUN check if false turns it off, or defaults to a different store. }; //To run a superpeer - cd wherever; node install gun; cd node_modules/gun; npm start - starts server by default on port 8080, or set an "env" - see http.js //setenv GUN_ENV false; node examples/http.js 4246 //Make sure to open of the port (typically in /etc/ferm) // TODO-GUN - copy example from systemctl here /* WORKING AROUND GUN WEIRNESS/SUBOPTIMAL (of course, whats weird/sub-optimal to me, might be ideal to someone else) WORKAROUND-GUN-UNDERSCORE .once() and possibly .on() send an extra GUN internal field "_" which needs filtering. Reported and hopefully will get fixed .once behaves differently on node or the browser - this is a bug https://github.com/amark/gun/issues/586 and for now this code doesnt work on Node WORKAROUND-GUN-CURRENT: .once() and .on() deliver existing values as well as changes, reported & hopefully will get way to find just new ones. WORKAROUND-GUN-DELETE: There is no way to delete an item, setting it to null is recorded and is by convention a deletion. BUT the field will still show up in .once and .on, WORKAROUND-GUN-PROMISES: GUN is not promisified, there is only one place we care, and that is .once (since .on is called multiple times). Errors and Promises: Note that GUN's use of promises is seriously uexpected (aka weird), see https://gun.eco/docs/SEA#errors instead of using .reject or throwing an error at async it puts the error in SEA.err, so how that works in async parallel context is anyone's guess */ class TransportGUN extends Transport { /* GUN specific transport - over IPFS Fields: gun: object returned when starting GUN */ constructor(options, verbose) { super(options, verbose); this.options = options; // Dictionary of options this.gun = undefined; this.name = "GUN"; // For console log etc this.supportURLs = ['gun']; this.supportFunctions = [ 'fetch', //'store' 'connection', 'get', 'set', 'getall', 'keys', 'newdatabase', 'newtable', 'monitor', 'add', 'list', 'listmonitor', 'newlisturls']; this.status = Transport.STATUS_LOADED; } connection(url, verbose) { /* TODO-GUN need to determine what a "rooted" Url is in gun, is it specific to a superpeer for example Utility function to get Gun object for this URL (note this isn't async) url: URL string to find list of of form [gun|dweb]:/gun//[/ k !== '_').sort().map(k => data[k]) : []; //See WORKAROUND-GUN-UNDERSCORE // .filter((obj) => (obj.signedby.includes(url))); // upper layers verify, which filters if (verbose) console.log("GUN.p_rawlist found", ...utils.consolearr(res)); return res; } catch(err) { console.log("TransportGUN.p_rawlist failed",err.message); throw(err); } } listmonitor(url, callback, {verbose=false, current=false}={}) { /* 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. url: string Identifier of list (as used by p_rawlist and "signedby" parameter of p_rawadd callback: function(obj) Callback for each new item added to the list obj is same format as p_rawlist or p_rawreverse current true if should send list of existing elements verbose: true for debugging output */ let g = this.connection(url, verbose); if (!current) { // See WORKAROUND-GUN-CURRENT have to keep an extra copy to compare for which calls are new. g.once(data => { this.monitored = data ? Object.keys(data) : []; // Keep a copy - could actually just keep high water mark unless getting partial knowledge of state of array. g.map().on((v, k) => { if (!(this.monitored.includes(k)) && (k !== '_')) { //See WORKAROUND-GUN-UNDERSCORE this.monitored.push(k); callback(JSON.parse(v)); } }); }); } else { g.map().on((v, k) => callback("set", k, JSON.parse(v))); } } // noinspection JSCheckFunctionSignatures async p_rawadd(url, sig, {verbose=false}={}) { /* Store a new list item, 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 list to post to :param Signature sig: Signature object containing at least: date - date of signing in ISO format, urls - array of urls for the object being signed signature - verifiable signature of date+urls signedby - urls of public key used for the signature :param boolean verbose: true for debugging output :resolve undefined: */ // noinspection JSUnresolvedVariable console.assert(url && sig.urls.length && sig.signature && sig.signedby.length, "TransportGUN.p_rawadd args", url, sig); if (verbose) console.log("TransportGUN.p_rawadd", typeof url === "string" ? url : url.href, sig); this.connection(url, verbose) .set( JSON.stringify( sig.preflight( Object.assign({}, sig)))); } // noinspection JSCheckFunctionSignatures async p_newlisturls(cl, {verbose=false}={}) { let u = await this._p_newgun(cl, {verbose}); return [ u, u]; } //=======KEY VALUE TABLES ======== // noinspection JSMethodCanBeStatic async _p_newgun(pubkey, {verbose=false}={}) { if (pubkey.hasOwnProperty("keypair")) pubkey = pubkey.keypair.signingexport(); // By this point pubkey should be an export of a public key of form xyz:abc where xyz // specifies the type of public key (NACL VERIFY being the only kind we expect currently) return `gun:/gun/${encodeURIComponent(pubkey)}`; } async p_newdatabase(pubkey, {verbose=false}={}) { /* Request a new database For GUN it doesnt actually create anything, just generates the URLs TODO-GUN simple version first - userid based on my keypair first, then switch to Gun's userid and its keypair Include gun/sea.js; user.create(,); user.auth(,); # See gun.eco/docs/Auth returns: {publicurl: "gun:/gun/", privateurl: "gun:/gun/"> */ let u = await this._p_newgun(pubkey, {verbose}); return {publicurl: u, privateurl: u}; } async p_newtable(pubkey, table, {verbose=false}={}) { /* Request a new table For GUN it doesnt actually create anything, just generates the URLs returns: {publicurl: "gun:/gun//
", privateurl: "gun:/gun//
"> */ if (!pubkey) throw new errors.CodingError("p_newtable currently requires a pubkey"); let database = await this.p_newdatabase(pubkey, {verbose}); // If have use cases without a database, then call p_newdatabase first return { privateurl: `${database.privateurl}/${table}`, publicurl: `${database.publicurl}/${table}`} // No action required to create it } async p_set(url, keyvalues, value, {verbose=false}={}) { // url = yjs:/yjs/database/table /* Set key values keyvalues: string (key) in which case value should be set there OR object in which case value is ignored */ let table = this.connection(url, verbose); if (typeof keyvalues === "string") { table.path(keyvalues).put(JSON.stringify(value)); } else { // Store all key-value pairs without destroying any other key/value pairs previously set console.assert(!Array.isArray(keyvalues), "TransportGUN - shouldnt be passsing an array as the keyvalues"); table.put( Object.keys(keyvalues).reduce( function(previous, key) { previous[key] = JSON.stringify(keyvalues[key]); return previous; }, {} )) } } async p_rawfetch(url, {verbose=false}={}) { let g = this.connection(url, verbose); // Goes all the way to the key let val = await this._p_once(g); return typeof val === "string" ? JSON.parse(val) : val; // This looks like it is sync (see same code on p_get and p_rawfetch) } async p_get(url, keys, {verbose=false}={}) { let table = this.connection(url, verbose); if (Array.isArray(keys)) { throw new errors.ToBeImplementedError("p_get(url, [keys]) isn't supported - because of ambiguity better to explicitly loop on set of keys or use getall and filter"); /* return keys.reduce(function(previous, key) { let val = table.get(key); previous[key] = typeof val === "string" ? JSON.parse(val) : val; // Handle undefined return previous; }, {}); */ } else { let val = await this._p_once(table.get(keys)); // Resolves to value return typeof val === "string" ? JSON.parse(val) : val; // This looks like it is sync (see same code on p_get and p_rawfetch) } } async p_delete(url, keys, {verbose=false}={}) { let table = this.connection(url, verbose); if (typeof keys === "string") { table.path(keys).put(null); } else { keys.map((key) => table.path(key).put(null)); // This looks like it is sync } } //WORKAROUND-GUN-PROMISE suggest p_once as a good single addition //TODO-GUN expand this to workaround Gun weirdness with errors. _p_once(gun) { // Note in some cases (e.g. p_getall) this will resolve to a object, others a string/number (p_get) return new Promise((resolve, reject) => gun.once(resolve)); } async p_keys(url, {verbose=false}={}) { let res = await this._p_once(this.connection(url, verbose)); return Object.keys(res) .filter(k=> (k !== '_') && (res[k] !== null)); //See WORKAROUND-GUN-UNDERSCORE and WORKAROUND-GUN-DELETE } async p_getall(url, {verbose=false}={}) { let res = await this._p_once(this.connection(url, verbose)); return Object.keys(res) .filter(k=> (k !== '_') && res[k] !== null) //See WORKAROUND-GUN-UNDERSCORE and WORKAROUND-GUN-DELETE .reduce( function(previous, key) { previous[key] = JSON.parse(res[key]); return previous; }, {}); } async monitor(url, callback, {verbose=false, current=false}={}) { /* Setup a callback called whenever an item is added to a list, typically it would be called immediately after a p_getall to get any more items not returned by p_getall. Stack: KVT()|KVT.p_new => KVT.monitor => (a: Transports.monitor => GUN.monitor)(b: dispatchEvent) url: string Identifier of list (as used by p_rawlist and "signedby" parameter of p_rawadd callback: function({type, key, value}) Callback for each new item added to the list (type = "set"|"delete") verbose: boolean - true for debugging output current Send existing items to the callback as well */ let g = this.connection(url, verbose); if (!current) { // See WORKAROUND-GUN-CURRENT have to keep an extra copy to compare for which calls are new. g.once(data => { this.monitored = Object.assign({},data); // Make a copy of data (this.monitored = data won't work as just points at same structure) g.map().on((v, k) => { if ((v !== this.monitored[k]) && (k !== '_')) { //See WORKAROUND-GUN-UNDERSCORE this.monitored[k] = v; callback("set", k, JSON.parse(v)); } }); }); } else { g.map().on((v, k) => callback("set", k, JSON.parse(v))); } } static async p_test(verbose) { if (verbose) {console.log("TransportGUN.test")} try { let t = this.setup0({}, verbose); //TODO-GUN when works with peers commented out, try passing peers: [] await t.p_setup1(verbose); // Not passing cb yet await t.p_setup2(verbose); // Not passing cb yet - this one does nothing on GUN // noinspection JSIgnoredPromiseFromCall t.p_test_kvt("gun:/gun/NACL", {verbose}); //t.p_test_list("gun:/gun/NACL", {verbose}); //TODO test_list needs fixing to not create a dependency on Signature } catch(err) { console.log("Exception thrown in TransportGUN.test:", err.message); throw err; } } static async demo_bugs() { let gun = new Gun(); gun.get('foo').get('bar').put('baz'); console.log("Expect {bar: 'baz'} but get {_:..., bar: 'baz'}"); gun.get('foo').once(data => console.log(data)); gun.get('zip').get('bar').set('alice'); console.log("Expect {12345: 'alice'} but get {_:..., 12345: 'alice'}"); gun.get('foo').once(data => console.log(data)); // Returns extra "_" field } } Transports._transportclasses["GUN"] = TransportGUN; exports = module.exports = TransportGUN;