diff --git a/CHANGELOG.md b/CHANGELOG.md index c8e523b0..f1e4ed7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # CHANGELOG +## 0.3.4 + + - Breaking Change! `list.set(item)` returns the item's chain now, not the list chain. + - Client and Server GUN servers are now more up to spec, trimmed excess HTTP/REST header data. + - Gun.is.lex added. + ## 0.3.3 - You can now link nodes natively, `gun.get('mark').path('owner').put(gun.get('cat'))`! diff --git a/gun.js b/gun.js index e6c422c0..65499909 100644 --- a/gun.js +++ b/gun.js @@ -27,7 +27,7 @@ } Type.text.match = function(t, o){ var r = false; t = t || ''; - o = o || {}; // {'~', '=', '*', '<', '>', '+', '-', '?', '!'} // ignore uppercase, exactly equal, anything after, lexically larger, lexically lesser, added in, subtacted from, questionable fuzzy match, and ends with. + o = Gun.text.is(o)? {'=': o} : o || {}; // {'~', '=', '*', '<', '>', '+', '-', '?', '!'} // ignore uppercase, exactly equal, anything after, lexically larger, lexically lesser, added in, subtacted from, questionable fuzzy match, and ends with. if(Type.obj.has(o,'~')){ t = t.toLowerCase() } if(Type.obj.has(o,'=')){ return t === o['='] } if(Type.obj.has(o,'*')){ if(t.slice(0, o['*'].length) === o['*']){ r = true; t = t.slice(o['*'].length) } else { return false }} @@ -205,6 +205,10 @@ ,soul: '#' // a soul is a UUID of a node but it always points to the "latest" data known. ,field: '.' // a field is a property on a node which points to a value. ,state: '>' // other than the soul, we store HAM metadata. + ,'#':'soul' + ,'.':'field' + ,'=':'value' + ,'>':'state' } Gun.is = function(gun){ return (gun instanceof Gun)? true : false } // check to see if it is a GUN instance. @@ -239,6 +243,14 @@ } Gun.is.rel.ify = function(s){ var r = {}; return Gun.obj.put(r, Gun._.soul, s), r } // convert a soul into a relation and return it. + + Gun.is.lex = function(l){ var r = true; + if(!Gun.obj.is(l)){ return false } + Gun.obj.map(l, function(v,f){ + if(!Gun.obj.has(Gun._,f) || !(Gun.text.is(v) || Gun.obj.is(v))){ return r = false } + }); // TODO: What if the lex cursor has a document on the match, that shouldn't be allowed! + return r; + } Gun.is.node = function(n, cb, t){ var s; // checks to see if an object is a valid node. if(!Gun.obj.is(n)){ return false } // must be an object. @@ -678,6 +690,7 @@ return true; } function stream(err, data, info){ + //console.log("wire.get <--", err, data); Gun.on('wire.get').emit(ctx.by.chain, ctx, err, data, info); if(err){ Gun.log(err.err || err); @@ -962,15 +975,17 @@ } Gun.chain.set = function(item, cb, opt){ - var gun = this, ctx = {}; - if(!Gun.is(item)){ return cb.call(gun, {err: Gun.log('Set only supports node references currently!')}), gun } - item.val(function(node){ + var gun = this, ctx = {}, chain; + if(!Gun.is(item)){ return cb.call(gun, {err: Gun.log('Set only supports node references currently!')}), gun } // TODO: Bug? Should we return not gun on error? + (ctx.chain = item.chain()).back = gun; + ctx.chain._ = item._; + item.val(function(node){ // TODO: BUG! Return proxy chain with back = list. if(ctx.done){ return } ctx.done = true; var put = {}, soul = Gun.is.node.soul(node); if(!soul){ return cb.call(gun, {err: Gun.log('Only a node can be linked! Not "' + node + '"!')}) } gun.put(Gun.obj.put(put, soul, Gun.is.rel.ify(soul)), cb, opt); - }) - return gun; + }); + return ctx.chain; } Gun.chain.init = function(cb, opt){ @@ -1125,7 +1140,7 @@ ;(function(exports){ function s(){} s.put = function(key, val){ return store.setItem(key, Gun.text.ify(val)) } - s.get = function(key, cb){ setTimeout(function(){ return cb(null, Gun.obj.ify(store.getItem(key) || null)) },1)} + s.get = function(key, cb){ /*setTimeout(function(){*/ return cb(null, Gun.obj.ify(store.getItem(key) || null)) /*},1)*/} s.del = function(key){ return store.removeItem(key) } var store = this.localStorage || {setItem: function(){}, removeItem: function(){}, getItem: function(){}}; exports.store = s; @@ -1136,6 +1151,7 @@ var tab = gun.tab = gun.tab || {}; tab.store = tab.store || Tab.store; tab.request = tab.request || request; + tab.request.s = tab.request.s || {}; tab.headers = opt.headers || {}; tab.headers['gun-sid'] = tab.headers['gun-sid'] || Gun.text.random(); // stream id tab.prefix = tab.prefix || opt.prefix || 'gun/'; @@ -1144,11 +1160,7 @@ var soul = lex[Gun._.soul]; if(!soul){ return } cb = cb || function(){}; - cb.GET = true; - (opt = opt || {}).url = opt.url || {}; - opt.headers = Gun.obj.copy(tab.headers); - opt.url.pathname = '/' + soul; - //Gun.log("tab get --->", lex); + (opt.headers = Gun.obj.copy(tab.headers)).id = tab.msg(); (function local(soul, cb){ tab.store.get(tab.prefix + soul, function(err, data){ if(!data){ return } // let the peers handle no data. @@ -1159,30 +1171,32 @@ }); }(soul, cb)); if(!(cb.local = opt.local)){ + tab.request.s[opt.headers.id] = tab.error(cb, "Error: Get failed!", function(reply){ + setTimeout(function(){ tab.put(Gun.is.graph.ify(reply.body), function(){}, {local: true, peers: {}}) },1); // and flush the in memory nodes of this graph to localStorage after we've had a chance to union on it. + }); Gun.obj.map(opt.peers || gun.__.opt.peers, function(peer, url){ var p = {}; - tab.request(url, null, tab.error(cb, "Error: Get failed through " + url, function(reply){ - if(!p.node && cb.node){ // if we have local data - //Gun.log("tab get <---", lex); - tab.put(Gun.is.graph.ify(p.node = cb.node), function(e,r){ // then sync it if we haven't already - //Gun.log("Stateless handshake sync:", e, r); - }, {peers: tab.peers(url)}); // to the peer. // TODO: This forces local to flush again, not necessary. - } - setTimeout(function(){ tab.put(reply.body, function(){}, {local: true}) },1); // and flush the in memory nodes of this graph to localStorage after we've had a chance to union on it. - }), opt); + tab.request(url, lex, tab.request.s[opt.headers.id], opt); cb.peers = true; }); + var node = gun.__.graph[soul]; + if(node){ + tab.put(Gun.is.graph.ify(node)); + } } tab.peers(cb); } tab.put = tab.put || function(graph, cb, opt){ + //console.log("SAVE", graph); cb = cb || function(){}; opt = opt || {}; + (opt.headers = Gun.obj.copy(tab.headers)).id = tab.msg(); Gun.is.graph(graph, function(node, soul){ if(!gun.__.graph[soul]){ return } tab.store.put(tab.prefix + soul, gun.__.graph[soul]); }); if(!(cb.local = opt.local)){ + tab.request.s[opt.headers.id] = tab.error(cb, "Error: Put failed!"); Gun.obj.map(opt.peers || gun.__.opt.peers, function(peer, url){ - tab.request(url, graph, tab.error(cb, "Error: Put failed on " + url), {headers: tab.headers}); + tab.request(url, graph, tab.request.s[opt.headers.id], opt); cb.peers = true; }); } tab.peers(cb); @@ -1204,22 +1218,46 @@ if(!(cb.graph || cb.node)){ cb(null) } },1)} } + tab.msg = tab.msg || function(id){ + if(!id){ + return tab.msg.debounce[id = Gun.text.random(9)] = Gun.time.is(), id; + } + clearTimeout(tab.msg.clear); + tab.msg.clear = setTimeout(function(){ + var now = Gun.time.is(); + Gun.obj.map(tab.msg.debounce, function(t,id){ + if(now - t < 1000 * 60 * 5){ return } + Gun.obj.del(tab.msg.debounce, id); + }); + },500); + if(id = tab.msg.debounce[id]){ + return tab.msg.debounce[id] = Gun.time.is(), id; + } + }; + tab.msg.debounce = tab.msg.debounce || {}; tab.server = tab.server || function(req, res){ - if(!req || !res || !req.url || !req.method){ return } - req.url = req.url.href? req.url : document.createElement('a'); - req.url.href = req.url.href || req.url; - req.url.key = (req.url.pathname||'').replace(tab.server.regex,'').replace(/^\//i,'') || ''; - req.method = req.body? 'put' : 'get'; - if('get' == req.method){ return tab.server.get(req, res) } - if('put' == req.method || 'post' == req.method){ return tab.server.put(req, res) } + if(!req || !res || !req.body || !req.headers || !req.headers.id){ return } + if(tab.request.s[req.headers.rid]){ return tab.request.s[req.headers.rid](null, req) } + if(tab.msg(req.headers.id)){ return } + // TODO: Re-emit message to other peers if we have any non-overlaping ones. + if(req.headers.rid){ return } // no need to process + if(Gun.is.lex(req.body)){ return tab.server.get(req, res) } + else { return tab.server.put(req, res) } } tab.server.json = 'application/json'; tab.server.regex = gun.__.opt.route = gun.__.opt.route || opt.route || /^\/gun/i; - tab.server.get = function(){} + tab.server.get = function(req, cb){ + var soul = req.body[Gun._.soul], node; + if(!(node = gun.__.graph[soul])){ return } + var reply = {headers: {'Content-Type': tab.server.json, rid: req.headers.id, id: tab.msg()}}; + cb({headers: reply.headers, body: node}); + } tab.server.put = function(req, cb){ - var reply = {headers: {'Content-Type': tab.server.json}}; + var reply = {headers: {'Content-Type': tab.server.json, rid: req.headers.id, id: tab.msg()}}, keep; if(!req.body){ return cb({headers: reply.headers, body: {err: "No body"}}) } - // TODO: Re-emit message to other peers if we have any non-overlaping ones. + if(!Gun.obj.is(req.body, function(node, soul){ + if(gun.__.graph[soul]){ return true } + })){ return } if(req.err = Gun.union(gun, req.body, function(err, ctx){ if(err){ return cb({headers: reply.headers, body: {err: err || "Union failed."}}) } var ctx = ctx || {}; ctx.graph = {}; @@ -1227,7 +1265,7 @@ gun.__.opt.wire.put(ctx.graph, function(err, ok){ if(err){ return cb({headers: reply.headers, body: {err: err || "Failed."}}) } cb({headers: reply.headers, body: {ok: ok || "Persisted."}}); - }, {local: true}); + }, {local: true, peers: {}}); }).err){ cb({headers: reply.headers, body: {err: req.err || "Union failed."}}) } } Gun.obj.map(gun.__.opt.peers, function(){ // only create server if peers and do it once by returning immediately. @@ -1243,6 +1281,7 @@ opt = opt || (base.length? {base: base} : base); opt.base = opt.base || base; opt.body = opt.body || body; + cb = cb || function(){}; if(!opt.base){ return } r.transport(opt, cb); } @@ -1300,7 +1339,7 @@ res.headers = res.headers || {}; if(res.headers['ws-rid']){ return (r.ws.cbs[res.headers['ws-rid']]||function(){})(null, res) } //Gun.log("We have a pushed message!", res); - if(res.body){ r.createServer.ing(res, function(){}) } // emit extra events. + if(res.body){ r.createServer.ing(res, function(res){ r(opt.base, null, null, res)}) } // emit extra events. }; ws.onerror = function(e){ Gun.log(e); }; return true; @@ -1345,7 +1384,7 @@ while(reply.body && reply.body.length && reply.body.shift){ // we're assuming an array rather than chunk encoding. :( var res = reply.body.shift(); //Gun.log("-- go go go", res); - if(res && res.body){ r.createServer.ing(res, function(){}) } // emit extra events. + if(res && res.body){ r.createServer.ing(res, function(){ r(opt.base, null, null, res) }) } // emit extra events. } }); }, res.headers.poll); diff --git a/lib/ws.js b/lib/ws.js index 97c93146..34960bc4 100644 --- a/lib/ws.js +++ b/lib/ws.js @@ -12,14 +12,14 @@ module.exports = function(wss, server){ msg = Gun.obj.ify(msg); msg.url = msg.url || {}; msg.url.pathname = (req.url.pathname||'') + (msg.url.pathname||''); - Gun.obj.map(req.url, function(val, i){ + /*Gun.obj.map(req.url, function(val, i){ msg.url[i] = msg.url[i] || val; // reattach url - }); + });*/ msg.method = msg.method || req.method; msg.headers = msg.headers || {}; - Gun.obj.map(req.headers, function(val, i){ + /*Gun.obj.map(req.headers, function(val, i){ msg.headers[i] = msg.headers[i] || val; // reattach headers - }); + });*/ server.call(ws, msg, function(reply){ if(!ws || !ws.send || !ws._socket || !ws._socket.writable){ return } reply = reply || {}; diff --git a/lib/wsp.js b/lib/wsp.js index c9abe44b..54280b20 100644 --- a/lib/wsp.js +++ b/lib/wsp.js @@ -92,20 +92,40 @@ if((gun.__.opt.maxSockets = opt.maxSockets || gun.__.opt.maxSockets) !== false){ require('https').globalAgent.maxSockets = require('http').globalAgent.maxSockets = gun.__.opt.maxSockets || Infinity; } + gun.wsp.msg = gun.wsp.msg || function(id){ + if(!id){ + return gun.wsp.msg.debounce[id = Gun.text.random(9)] = Gun.time.is(), id; + } + clearTimeout(gun.wsp.msg.clear); + gun.wsp.msg.clear = setTimeout(function(){ + var now = Gun.time.is(); + Gun.obj.map(gun.wsp.msg.debounce, function(t,id){ + if((now - t) < (1000 * 60 * 5)){ return } + Gun.obj.del(gun.wsp.msg.debounce, id); + }); + },500); + if(id = gun.wsp.msg.debounce[id]){ + return gun.wsp.msg.debounce[id] = Gun.time.is(), id; + } + }; + gun.wsp.msg.debounce = gun.wsp.msg.debounce || {}; gun.wsp.wire = gun.wsp.wire || (function(){ // all streams, technically PATCH but implemented as PUT or POST, are forwarded to other trusted peers // except for the ones that are listed in the message as having already been sending to. // all states, implemented with GET, are replied to the source that asked for it. - function tran(req, cb){ - req.method = req.body? 'put' : 'get'; // put or get is based on whether there is a body or not - req.url.key = req.url.pathname.replace(gun.wsp.regex,'').replace(/^\//i,'') || ''; - if('get' == req.method){ return tran.get(req, cb) } - if('put' == req.method || 'post' == req.method){ return tran.put(req, cb) } + function tran(req, res){ + if(!req || !res || !req.body || !req.headers || !req.headers.id){ return } + if(gun.wsp.msg(req.headers.id)){ return } + req.method = req.body? 'put' : 'get'; + gun.wsp.on('network').emit(Gun.obj.copy(req)); + if(req.headers.rid){ return } // no need to process. + if(Gun.is.lex(req.body)){ return tran.get(req, res) } + else { return tran.put(req, res) } cb({body: {hello: 'world'}}); } tran.get = function(req, cb){ var key = req.url.key - , reply = {headers: {'Content-Type': tran.json}}; + , reply = {headers: {'Content-Type': tran.json, rid: req.headers.id, id: gun.wsp.msg()}}; //console.log(req); // NTS HACK! SHOULD BE ITS OWN ISOLATED MODULE! // if(req && req.url && req.url.pathname && req.url.pathname.indexOf('gun.nts') >= 0){ @@ -118,22 +138,14 @@ cb({headers: reply.headers, body: (err? (err.err? err : {err: err || "Unknown error."}) : list || null ) }) }); } - // END ALL HACK! // - if(!key){ - if(!Gun.obj.has(req.url.query, Gun._.soul)){ - return cb({headers: reply.headers, body: {err: "No key or soul to get."}}); - } - key = {}; - key[Gun._.soul] = req.url.query[Gun._.soul]; - } - if(Gun.text.is(key)){ - key = Gun.is.rel.ify(key); - } - //console.log("tran.get", key); + console.log("GET!", req); + key = req.body; + console.log("tran.get", key); var opt = {key: false}; //gun.get(key, function(err, node){ (gun.__.opt.wire.get||function(key, cb){cb(null,null)})(key, function(err, node){ //console.log("tran.get", key, "<---", err, node); + reply.headers.id = gun.wsp.msg(); if(err || !node){ if(opt.on && opt.on.off){ opt.on.off() } return cb({headers: reply.headers, body: (err? (err.err? err : {err: err || "Unknown error."}) : null)}); @@ -168,9 +180,8 @@ tran.put = function(req, cb){ // NOTE: It is highly recommended you do your own PUT/POSTs through your own API that then saves to gun manually. // This will give you much more fine-grain control over security, transactions, and what not. - var reply = {headers: {'Content-Type': tran.json}}; + var reply = {headers: {'Content-Type': tran.json, rid: req.headers.id, id: gun.wsp.msg()}}; if(!req.body){ return cb({headers: reply.headers, body: {err: "No body"}}) } - gun.wsp.on('network').emit(Gun.obj.copy(req)); //console.log("\n\ntran.put ----------------->", req.body); if(Gun.is.graph(req.body)){ if(req.err = Gun.union(gun, req.body, function(err, ctx){ // TODO: BUG? Probably should give me ctx.graph diff --git a/package.json b/package.json index ca0dc823..550e3a2b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gun", - "version": "0.3.3", + "version": "0.3.4", "description": "Graph engine", "main": "index.js", "scripts": { diff --git a/test/common.js b/test/common.js index 7306f26d..f4ff81e4 100644 --- a/test/common.js +++ b/test/common.js @@ -435,7 +435,7 @@ describe('Gun', function(){ expect(Gun.is.val({a:1})).to.be(false); expect(Gun.is.val(function(){})).to.be(false); }); - it('is soul',function(){ + it('is rel',function(){ expect(Gun.is.rel({'#':'somesoulidhere'})).to.be('somesoulidhere'); expect(Gun.is.rel({'#':'somethingelsehere'})).to.be('somethingelsehere'); expect(Gun.is.rel({'#':'somesoulidhere', and: 'nope'})).to.be(false); @@ -454,6 +454,17 @@ describe('Gun', function(){ expect(Gun.is.rel({a:1})).to.be(false); expect(Gun.is.rel(function(){})).to.be(false); }); + it('is lex',function(){ + expect(Gun.is.lex({'#': 'soul'})).to.be(true); + expect(Gun.is.lex({'.': 'field'})).to.be(true); + expect(Gun.is.lex({'=': 'value'})).to.be(true); + expect(Gun.is.lex({'>': 'state'})).to.be(true); + expect(Gun.is.lex({'#': {'=': 'soul'}})).to.be(true); + expect(Gun.is.lex({'#': {'=': 'soul'}, '.': []})).to.be(false); + expect(Gun.is.lex({'#': {'=': 'soul'}, 'asdf': 'oye'})).to.be(false); + expect(Gun.is.lex()).to.be(false); + expect(Gun.is.lex('')).to.be(false); + }); it('is node',function(){ expect(Gun.is.node({_:{'#':'somesoulidhere'}})).to.be(true); expect(Gun.is.node({_:{'#':'somesoulidhere'}, a:0, b: 1, c: '', d: 'e', f: {'#':'somethingelsehere'}})).to.be(true); @@ -3736,14 +3747,18 @@ describe('Gun', function(){ var carl = gun.put({name: 'carl', birth: Math.random()}).key('person/carl'); var dave = gun.put({name: 'dave', birth: Math.random()}).key('person/dave'); - users.set(alice).set(bob).set(carl).set(dave); + users.set(alice); + users.set(bob); + users.set(carl); + users.set(dave); - alice.path('friends').set(bob).set(carl); + alice.path('friends').set(bob).back.set(carl); bob.path('friends').set(alice); - dave.path('friends').set(alice).set(carl); + dave.path('friends').set(alice).back.set(carl); var team = gun.get('team/lions').put({name: "Lions"}); - team.path('members').set(alice).set(bob); + team.path('members').set(alice); + team.path('members').set(bob); alice.path('team').put(team); bob.path('team').put(team); diff --git a/test/mocha.html b/test/mocha.html index 3815c5d7..278135b9 100644 --- a/test/mocha.html +++ b/test/mocha.html @@ -1,6 +1,6 @@ - Gun Tests + GUN Tests @@ -14,7 +14,8 @@ - Gun Tests +

GUN Tests

+ Warning! You cannot run these tests in the background, this tab must be in active focus.