2018-07-06 20:41:45 -07:00
/ *
This Transport layers uses GUN .
* /
const Url = require ( 'url' ) ;
const Gun = require ( 'gun' )
// Utility packages (ours) And one-liners
function delay ( ms , val ) { return new Promise ( resolve => { setTimeout ( ( ) => { resolve ( val ) ; } , ms ) } ) }
// 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
let defaultoptions = {
2018-07-09 19:08:56 -07:00
peers : [ "http://xxxx:yyyy/gun" ]
//localstore: true #True is default
2018-07-06 20:41:45 -07:00
}
2018-07-09 19:08:56 -07:00
# TODO - GUN check dweb - objects for calls to monitor or listmonitor and make sure put { verbose } instead of "verbose"
# TODO - GUN - setup superpeer - mkdir ; node install gun ; cd node _modules / gun / server ; npm start - starts server by default on port 8080 , or set an "env" - see http . js
2018-07-06 20:41:45 -07:00
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 { ipfs: {...}, "yarrays", yarray: {...} }
this . gun = undefined ;
this . name = "GUN" ; // For console log etc
this . supportURLs = [ 'gun' ] ;
2018-07-09 19:08:56 -07:00
# TODO - GUN doesnt really support lists yet , its "set" function only handles other gun objects and doesnt order them
this . supportFunctions = [ 'connection' , 'get' , 'set' , 'getall' , 'keys' , 'newdatabase' , 'newtable' , 'monitor' ] ;
//Not supporting lists or blobs: ['fetch', 'add', 'list', 'listmonitor', 'newlisturls',]
2018-07-06 20:41:45 -07:00
this . status = Transport . STATUS _LOADED ;
}
async p _connection ( url , verbose ) {
/ *
Utility function to get Gun object for this URL
url : URL string to find list of
2018-07-09 19:08:56 -07:00
resolves : Gun a connection to use for get ' s etc , undefined if fails
2018-07-06 20:41:45 -07:00
* /
if ( typeof url === "string" )
url = Url . parse ( url ) ;
patharray = url . pathstring . split ( '/' ) //[ 'gun', database, table ]
patharray . shift ; // Loose "gun"
2018-07-09 19:08:56 -07:00
g = this . gun . path ( patharray ) ; // Not sure how this could become undefined as it will return g before the path is walked, but if do a lookup on this "g" then should get undefined
2018-07-06 20:41:45 -07:00
return g ;
}
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 .
options : { gun : { } , } Set of options - "gun" is used for those to pass direct to Gun
* /
let combinedoptions = Transport . mergeoptions ( defaultoptions , options ) ;
console . log ( "GUN options %o" , combinedoptions ) ; // Log even if !verbose
2018-07-09 19:08:56 -07:00
let t = new TransportGUN ( combinedoptions , verbose ) ; // Note doesnt start IPFS or OrbitDB
t . gun = new Gun ( t . options . gun ) ; // This doesnt connect, just creates db structure
2018-07-06 20:41:45 -07:00
Dweb . Transports . addtransport ( t ) ;
return t ;
}
async p _setup1 ( verbose , cb ) {
/ *
This sets up for GUN .
Throws : TODO - GUN document errors that can occur
* /
try {
this . status = Dweb . Transport . STATUS _STARTING ; // Should display, but probably not refreshed in most case
if ( cb ) cb ( this ) ;
2018-07-09 19:08:56 -07:00
//TODO-GUN-TEST - try connect and retrieve info then look at ._.opt.peers
2018-07-06 20:41:45 -07:00
await this . p _status ( verbose ) ;
} catch ( err ) {
console . error ( this . name , "failed to start" , err ) ;
this . status = Transport . STATUS _FAILED ;
}
if ( cb ) cb ( this ) ;
return this ;
}
async p _status ( verbose ) {
/ *
2018-07-09 19:08:56 -07:00
Return an integer for the status of a transport see Transport
2018-07-06 20:41:45 -07:00
* /
2018-07-09 19:08:56 -07:00
//TODO-GUN-TEST - try connect and retrieve info then look at ._.opt.peers
this . status = Transport . STATUS _CONNECTED ; //TODO-GUN how do I know if/when I'm connected (see comment on p_setup1 as well)
2018-07-06 20:41:45 -07:00
return this . status ;
}
2018-07-09 19:08:56 -07:00
// ===== LISTS ========
2018-07-06 20:41:45 -07:00
2018-07-09 19:08:56 -07:00
async 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 .
* /
try {
let g = await this . p _connection ( url , verbose ) ;
let res = g . once ( data => Object . keys ( data ) . sort ( ) . map ( k => data [ k ] ) )
// .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 } = { } ) {
2018-07-06 20:41:45 -07:00
/ *
2018-07-09 19:08:56 -07:00
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 .
2018-07-06 20:41:45 -07:00
2018-07-09 19:08:56 -07:00
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 = await this . p _connection ( url , verbose ) ;
if ( ! current ) {
g . once ( data => this . monitored = 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 ( v !== this . monitored [ k ] ) {
this . monitored [ k ] = v ;
callback ( JSON . parse ( v ) ) ;
}
} ) ;
} else {
g . map . on ( ( v , k ) => callback ( "set" , k , JSON . parse ( v ) ) ) ;
}
}
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 :
2018-07-06 20:41:45 -07:00
* /
2018-07-09 19:08:56 -07:00
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 ) ;
( await this . p _connection ( url , verbose )
. set ( JSON . stringify ( sig . preflight ( Object . assign ( { } , sig ) ) ) ) ;
}
async p _newlisturls ( cl , { verbose = false } = { } ) {
return await this . _p _newgun ( pubkey , { verbose } ) ;
}
//=======KEY VALUE TABLES ========
async _p _newgun ( pubkey , { verbose = false } = { } ) {
2018-07-06 20:41:45 -07:00
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)
let u = ` gun:/gun/ ${ encodeURIComponent ( pubkey ) } ` ;
return { "publicurl" : u , "privateurl" : u } ;
}
2018-07-09 19:08:56 -07:00
async p _newdatabase ( pubkey , { verbose = false } = { } ) {
/ *
Request a new database
For GUN it doesnt actually create anything , just generates the URLs
TODO - GUN - TODO simple version first - userid based on my keypair first , then switch to Gun ' s userid and its keypair
Include gun / sea . js ; user . create ( < alias > , < passphrase > ) ; user . auth ( < alias > , < passphrase > ) ; # See gun . eco / docs / Auth
returns : { publicurl : "gun:/gun/<publickey>" , privateurl : "gun:/gun/<publickey>" >
* /
return await this . _p _newgun ( pubkey , { verbose } ) ;
}
2018-07-06 20:41:45 -07:00
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/<publickey>/<table>" , privateurl : "gun:/gun/<publickey>/<table>" >
* /
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 = await this . p _connection ( url , verbose ) ;
if ( typeof keyvalues === "string" ) {
table . path ( keyvalues ) . put ( JSON . stringify ( value ) ) ;
} else {
table . put ( keyvalues ) ; // Store all key-value pairs without destroying any other key/value pairs previously set
}
}
async p _get ( url , keys , { verbose = false } = { } ) {
let table = await this . p _connection ( url , verbose ) ;
if ( Array . isArray ( keys ) ) {
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 = table . get ( keys ) ;
return typeof val === "string" ? JSON . parse ( val ) : val ; // This looks like it is sync
}
}
async p _delete ( url , keys , { verbose = false } = { } ) {
let table = await this . p _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
}
}
_p _once ( table ) {
return new Promise ( ( resolve , reject ) => table . once ( resolve ) ) ;
}
async p _keys ( url , { verbose = false } = { } ) {
kvs = await this . p _getall ( url , { verbose } ) ;
return Object . keys ( kvs ) ;
}
async p _getall ( url , { verbose = false } = { } ) {
2018-07-09 19:08:56 -07:00
return this . _p _once ( await this . p _connection ( url , verbose ) ) ;
2018-07-06 20:41:45 -07:00
}
2018-07-09 19:08:56 -07:00
async monitor ( url , callback , { verbose = false , current = false } = { } ) {
2018-07-06 20:41:45 -07:00
/ *
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 )
2018-07-09 19:08:56 -07:00
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" )
2018-07-06 20:41:45 -07:00
2018-07-09 19:08:56 -07:00
verbose : boolean - true for debugging output
current Send existing items to the callback as well
2018-07-06 20:41:45 -07:00
* /
# See https : //github.com/amark/gun/wiki/API#map for why this
# What we really want is to have the callback called once for each changed BUT
# conn . map ( ) . on ( cb ) will also get called for each initial value
# conn . on ( cb ) and then throwing away initial call would be ok , except it streams so cb might be called with first half of data and then rest
# TODO - GUN - waiting on an option for the above to have compliant monitor , for now just make sure to ignore dupes and note that GUN doesnt support list / add / listmonitor anyway
2018-07-09 19:08:56 -07:00
let g = await this . p _connection ( url , verbose ) ;
if ( ! current ) {
g . once ( data => this . monitored = data ) ; // Keep a copy
g . map . on ( ( v , k ) => {
if ( v !== this . monitored [ k ] ) {
this . monitored [ k ] = v ;
callback ( "set" , k , JSON . parse ( v ) ) ) ;
}
} ) ;
} else {
g . map . on ( ( v , k ) => callback ( "set" , k , JSON . parse ( v ) ) ) ;
}
2018-07-06 20:41:45 -07:00
}
2018-07-09 19:08:56 -07:00
static async p _test ( verbose ) { //TODO-GUN rewrite this based on code in YJS
2018-07-06 20:41:45 -07:00
if ( verbose ) { console . log ( "TransportGUN.test" ) }
try {
2018-07-09 19:08:56 -07:00
let t = this . setup0 ( { } , verbose ) ;
await t . p _setup1 ( verbose ) ; // Not passing cb yet
await t . p _setup2 ( verbose ) ; // Not passing cb yet - this one does nothing on GUN
this . p _test _kvt ( "XXX" , { verbose } ) ;
2018-07-06 20:41:45 -07:00
} catch ( err ) {
2018-07-09 19:08:56 -07:00
console . log ( "Exception thrown in TransportGUN.test:" , err . message ) ;
2018-07-06 20:41:45 -07:00
throw err ;
}
}
}
Transports . _transportclasses [ "GUN" ] = TransportGUN ;
exports = module . exports = TransportGUN ;