diff --git a/examples/admin/app.js b/examples/admin/app.js index 45845522..169d7eb7 100644 --- a/examples/admin/app.js +++ b/examples/admin/app.js @@ -17,8 +17,8 @@ app.listen(port); console.log('Express started on port ' + port + ' with /gun'); -gun.load('blob/data', function(){ // ugly little idempotent initializer, in case no data is set - gun.set({_:{'#': "yVbyf7BqlXVQQUOE5cw9rf8h",'>':{hello: 1407328713707,from: 1407328713707}}, // this is an actual gun object, this won't overwrite any new changes +gun.load('blob/data', function(){ // ugh need to initialize the data if there is none, what a waste of LOC! + gun.set({_:{'#': "yVbyf7BqlXVQQUOE5cw9rf8h",'>':{hello: 1407328713707,from: 1407328713707}}, // this is a nasty trick to force the ID to overwrite itself hello: "world", from: "Mark Nadal" }).key('blob/data'); diff --git a/examples/admin/index.html b/examples/admin/index.html index 03266d36..4b76723f 100644 --- a/examples/admin/index.html +++ b/examples/admin/index.html @@ -55,6 +55,12 @@ $scope.$data = gun.load('blob/data', function(data){ $scope.data = data; $scope.$apply(); + Gun.on(data._[Gun.sym.id]).event(function(node){ // one liner-ify this! + Gun.obj.map(node, function(val, field){ + $scope.data[field] = val; + }); + $scope.$apply(); + }); }); $scope.add = function(a,b,c){ $scope.$data.path($scope.field).set( diff --git a/gun.js b/gun.js index c9cc64e8..ab826009 100644 --- a/gun.js +++ b/gun.js @@ -4,12 +4,12 @@ if(!Gun.is(gun)){ return new Gun(opt); } - gun.init(opt); + gun.opt(opt); } Gun.is = function(gun){ return (gun instanceof Gun)? true : false } Gun._ = {}; Gun.chain = Gun.prototype; - Gun.chain.init = function(opt, stun){ // idempotently update or set options + Gun.chain.opt = function(opt, stun){ // idempotently update or set options var gun = this; gun._ = gun._ || {}; gun.__ = gun.__ || {}; @@ -28,7 +28,7 @@ if(!Gun.fns.is(h)){ return } gun.__.opt.hook[f] = h; }); - if(!stun){ Gun.on('init').emit(gun, opt) } + if(!stun){ Gun.on('opt').emit(gun, opt) } return gun; } Gun.chain.chain = function(from){ @@ -225,6 +225,7 @@ Gun.text = {}; Gun.text.is = function(t){ return typeof t == 'string'? true : false } Gun.text.ify = function(t){ + if(Gun.text.is(t)){ return t } if(JSON){ return JSON.stringify(t) } return (t && t.toString)? t.toString() : t; } @@ -363,7 +364,7 @@ }); } Gun.HAM = function(current, delta, some){ // TODO: BUG! HAM on sub-graphs has not yet been put into code, thus divergences could occur - this is alpha! - function HAM(machineState, incomingState, currentState, incomingValue, currentValue){ + function HAM(machineState, incomingState, currentState, incomingValue, currentValue){ // TODO: Lester's comments on roll backs could be vulnerable to divergence, investigate! if(machineState < incomingState){ // the incoming value is outside the boundary of the machine's state, it must be reprocessed in another state. return {amnesiaQuarantine: true}; @@ -609,34 +610,94 @@ } }({})); -;(function(Page){ +;(function(tab){ if(!this.Gun){ return } if(!window.JSON){ Gun.log("Include JSON first: ajax.cdnjs.com/ajax/libs/json2/20110223/json2.js") } // for old IE use - Gun.on('init').event(function(gun, opt){ - Page.load = function(key, cb, opt){ + Gun.on('opt').event(function(gun, opt){ + tab.server = tab.server || function(req, res, next){ + + } + tab.load = tab.load || function(key, cb, opt){ cb = cb || function(){}; opt = opt || {}; Gun.obj.map(gun.__.opt.peers, function(peer, url){ - Page.ajax(url + '/' + key, null, function(data){ - Gun.log('via', url, key, data); - // alert(data + data.hello + data.from + data._); - cb(null, data); - if(!data || !data._){ return } - Page.subscribe(data._[Gun.sym.id]); - }); + tab.ajax(url + '/' + key, null, function(reply){ + Gun.log('via', url, key, reply); + if(!reply){ return } // handle reconnect? + if(reply.body && reply.body.err){ + cb(reply.body.err); + } else { + cb(null, reply.body); + } + + (function(){ + tab.subscribe.sub = (reply.headers || {})['Gun-Sub']; + var data = reply.body; + if(!data || !data._){ return } + tab.subscribe(data._[Gun.sym.id]); + }()); + }, {head: {'Gun-Sub': 1}}); }); } - Page.set = function(nodes, cb){ + tab.set = tab.set || function(nodes, cb){ cb = cb || function(){}; // TODO: batch and throttle later. console.log('ajax set', nodes); Gun.obj.map(gun.__.opt.peers, function(peer, url){ - Page.ajax(url, nodes, function(reply){ + tab.ajax(url, nodes, function(reply){ console.log("set confirmed?", reply); }); }); } - Page.query = function(params){ + tab.subscribe = function(id){ + tab.subscribe.to = tab.subscribe.to || {}; + if(id){ + tab.subscribe.to[id] = 1; + } + var opt = { + //head: {'Gun-Sub': 1}, + headers: { + 'Gun-Transport': 'XHR-SLP', + 'Gun-Sub': tab.subscribe.sub || '' + } + }, query = tab.subscribe.sub? '' : tab.subscribe.query(tab.subscribe.to); + console.log("SUB", tab.subscribe.sub); + Gun.obj.map(gun.__.opt.peers, function(peer, url){ + tab.ajax(url + query, null, function(reply){ + tab.subscribe.poll(); + if(!reply){ return } // do anything? + if(reply.headers){ + tab.subscribe.sub = reply.headers['Gun-Sub'] || tab.subscribe.sub; + } + var data = reply.body + , union = function(node){ // maybe we shouldn't have this type of logic, below, in a hook? + // should we pass it off to a gun API? same with everywhere else this shows up then. + if(!node || !node._ || !node._[Gun.sym.id]){ return } // do anything? + var context = {nodes: {}}; + context.nodes[node._[Gun.sym.id]] = node; + context = Gun.chain.set.now.union.call(gun, context.nodes); + if(context.err){ return } // do anything? + Gun.obj.map(context.nodes, function(node, id){ + Gun.on(id).emit(node); // TODO: we shouldn't use Gun's global event namespace like this, change to local + }); + } + if(!data){ return } // do anything? + if(data.err){ return } // do anything? + if(data._){ + union(data); + } else { + Gun.obj.map(data, function(node, id){ + union(node); + }); + } + }, opt); + }); + } + tab.subscribe.poll = function(){ + clearTimeout(tab.subscribe.poll.id); + tab.subscribe.poll.id = setTimeout(tab.subscribe, 1); // 1000 * 10); // should enable some server-side control of this. + } + tab.subscribe.query = function(params){ var s = '?' , uri = encodeURIComponent; Gun.obj.map(params, function(val, field){ @@ -644,18 +705,7 @@ }); return s; } - Page.subscribe = function(id){ - Page.subscribe.to = Page.subscribe.to || {}; - Page.subscribe.to[id] = 1; - var query = Page.query(Page.subscribe.to) || ''; - Gun.obj.map(gun.__.opt.peers, function(peer, url){ - Page.ajax(url + query, null, function(data){ - console.log("subscribe reply!", data); - }); - }); - console.log("live", query); - } - Page.ajax = + tab.ajax = window.ajax = function(url, data, cb, opt){ /* @@ -668,11 +718,13 @@ */ var u; opt = opt || {}; + opt.head = opt.head || {}; + opt.reshead = {}; + opt.headers = opt.headers || {}; if(data === u || data === null){ data = u; } else { try{data = JSON.stringify(data); - opt.headers = opt.headers || {}; opt.headers["Content-Type"] = "application/json;charset=utf-8"; }catch(e){} } @@ -694,20 +746,24 @@ } opt.xhr = null; } - opt.data = opt.data || function(d){ - var t; - try{t = JSON.parse(d) || d; + opt.data = opt.data || function(d, head){ + var reply = {}; + reply.headers = head; + try{reply.body = JSON.parse(d) || d; }catch(e){ - t = d; + reply.body = d; } - if(cb){ cb(t) } + if(cb){ cb(reply) } } opt.chunk = function(status, text, force){ if(status !== 200){ return } + opt.each(opt.head, function(val, i){ + opt.reshead[i] = opt.xhr.getResponseHeader(i); + }); var d, b, p = 1; while(p || force){ if(u !== d){ - opt.data(d); + opt.data(d, opt.reshead); force = false; } b = text.slice(opt.i = opt.i || 0); @@ -771,12 +827,18 @@ opt.error(); return; } - if(opt.headers){ - try{for(var i in opt.headers){ - if(opt.headers.hasOwnProperty(i)){ - opt.xhr.setRequestHeader(i, opt.headers[i]); - } + opt.each = function(obj, cb){ + if(!obj || !cb){ return } + for(var i in obj){ + if(obj.hasOwnProperty(i)){ + cb(obj[i], i); } + } + } + if(opt.headers){ + try{opt.each(opt.headers, function(val, i){ + opt.xhr.setRequestHeader(i, val); + }); } catch(e) { opt.error(); return; @@ -788,7 +850,7 @@ } return opt; } - gun.__.opt.hook.load = gun.__.opt.hook.load || Page.load; - gun.__.opt.hook.set = gun.__.opt.hook.set || Page.set; + gun.__.opt.hook.load = gun.__.opt.hook.load || tab.load; + gun.__.opt.hook.set = gun.__.opt.hook.set || tab.set; }); }({})); \ No newline at end of file diff --git a/package.json b/package.json index 0d5d36ae..a3d58b8b 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { "name": "gun" -, "version": "0.0.2b" +, "version": "0.0.3" , "author": "Mark Nadal" , "description": "Graph engine." , "engines": { diff --git a/shots.js b/shots.js index 0eccf01a..c01411ad 100644 --- a/shots.js +++ b/shots.js @@ -3,62 +3,56 @@ , S3 = require(__dirname+'/gate/s3') // redis has been removed, can be replaced with a disk system , url = require('url') , meta = {}; - Gun.on('init').event(function(gun, opt){ + Gun.on('opt').event(function(gun, opt){ gun.server = gun.server || function(req, res, next){ // this whole function needs refactoring and modularization next = next || function(){}; if(!req || !res){ return next() } if(!req.url){ return next() } if(!req.method){ return next() } - var tmp = {}; - tmp.url = url.parse(req.url, true); - if(!gun.server.regex.test(tmp.url.pathname)){ return next() } - tmp.key = tmp.url.pathname.replace(gun.server.regex,'') || ''; - if(tmp.key.toLowerCase() === '.js'){ + var msg = {}; + msg.url = url.parse(req.url, true); + if(!gun.server.regex.test(msg.url.pathname)){ return next() } + msg.url.key = msg.url.pathname.replace(gun.server.regex,'') || ''; + if(msg.url.key.toLowerCase() === '.js'){ res.writeHead(200, {'Content-Type': 'text/javascript'}); res.end(gun.server.js = gun.server.js || require('fs').readFileSync(__dirname + '/gun.js')); // gun server is caching the gun library for the client return; } - console.log("\ngun server has requests!", req.method, req.url, req.headers, req.body); - tmp.key = tmp.key.replace(/^\//i,'') || ''; // strip the base - tmp.method = (req.method||'').toLowerCase(); - if('get' === tmp.method){ // get is used as subscribe - console.log("URL?", tmp.url); - if(tmp.url && tmp.url.query){ - /* - long polling! Idea: On data-flush or res.end, issue a timeout token, - that keeps the 'connection' alive even while disconnected. - Subsequent requests use the timeout token and thus continue off as before, seamlessly. - If after the timeout no follow up has been made, we assume the client has dropped / disconnected. - */ - Gun.obj.map(tmp.url.query, function(){ - tmp.query = true; - // subscribe this req/res to the ids, then make POSTS publish to them and reply! - // MARK! COME BACK HERE - }); - if(tmp.query){ - return; // we'll wait until we get updates before we reply. Long polling (should probably be implemented as a hook itself! so websockets can replace it) + msg.url.key = msg.url.key.replace(/^\//i,'') || ''; // strip the base + msg.method = (req.method||'').toLowerCase(); + msg.headers = req.headers; + msg.body = req.body; // TODO: include body-parser here? + if('get' === msg.method){ // get is used as subscribe + gun.__.opt.hook.sub(msg, function(reply){ + if(!res){ return } + if(!reply){ return res.end() } + if(reply.headers){ + if(!res._headerSent){ + Gun.obj.map(reply.headers, function(val, field){ + res.setHeader(field, val); + }); + } } - } - if(!tmp.key){ - return meta.JSON(res, {gun: true}); - } - // raw test for now, no auth: - gun.load(tmp.key, function(err, data){ - meta.CORS(req, res); - return meta.JSON(res, data || err); - }) + meta.CORS(req, res); // add option to disable this + if(reply.chunk){ + res.write(Gun.text.ify(reply.chunk)); + } + if(reply.body){ + res.end(Gun.text.ify(reply.body)); + } + }); + return; } else - if('post' === tmp.method || 'patch' === tmp.method){ // post is used as patch, sad that patch has such poor support - if(!req.body){ + if('post' === msg.method || 'patch' === msg.method){ // post is used as patch, sad that patch has such poor support + if(!msg.body){ console.log("Warn: No body on POST?"); } // raw test for now, no auth: // should probably load all the nodes first? - var context = Gun.chain.set.now.union.call(gun, req.body); // data safely transformed + var context = Gun.chain.set.now.union.call(gun, msg.body); // data safely transformed if(context.err){ - return meta.JSON(res, context.err); // need to standardize errors more + return meta.JSON(res, context.err); // need to use the now standardized errors } - // console.log("-------- union ---------");Gun.obj.map(gun.__.nodes, function(node){ console.log(node); });console.log("------------------------"); /* WARNING! TODO: BUG! Do not send OK confirmation if amnesiaQuaratine is activated! Not until after it has actually been processed!!! */ @@ -73,15 +67,26 @@ context.err = "Warning! You have no persistence layer to save to!"; Gun.log(context.err); } + + var diff = msg.body + msg.body = null; + Gun.obj.map(context.nodes, function(node, id){ + var req = Gun.obj.copy(msg); + msg.body = node; + gun.server.push.on(id).emit(msg); + }); + msg.body = diff; } } gun.server.regex = /^\/gun/i; + gun.server.clients = {}; + gun.server.push = Gun.on.split(); var s3 = gun.__.opt.s3 = gun.__.opt.s3 || S3(opt && opt.s3); s3.prefix = s3.prefix || opt.s3.prefix || ''; s3.prekey = s3.prekey || opt.s3.prekey || ''; s3.prenode = s3.prenode || opt.s3.prenode || '_/nodes/'; gun.__.opt.batch = opt.batch || gun.__.opt.batch || 10; - gun.__.opt.throttle = opt.throttle || gun.__.opt.throttle || 2; + gun.__.opt.throttle = opt.throttle || gun.__.opt.throttle || 15; if(!gun.__.opt.keepMaxSockets){ require('https').globalAgent.maxSockets = require('http').globalAgent.maxSockets = Infinity } // WARNING: Document this! s3.load = s3.load || function(key, cb, opt){ @@ -174,17 +179,104 @@ }, {Metadata: {'#': id}}); } + gun.server.sub = (function(){ + function sub(req, cb){ + //console.log("\n\n\n", req); + req.sub = req.headers['gun-sub']; + req.transport = req.headers['gun-transport']; + if(req.transport === 'XHR-SLP'){ return sub.SLP(req, cb) } + if(!req.url.key){ return sub.keyless(req, cb) } + // raw test for now, no auth: + req.tab = sub.s[req.sub] || {}; + cb.header = {'Content-Type': sub.json}; + cb.header['Gun-Sub'] = req.tab.sub = + req.sub = req.tab.sub || req.sub || Gun.text.random(); + gun.load(req.url.key, function(node){ + sub.scribe(req.tab, node._[Gun.sym.id]); + cb({ + headers: cb.header + ,body: Gun.text.ify(node) + }); + }).blank(function(){ + cb({ + headers: cb.header + ,body: Gun.text.ify(null) + }); + }).dud(function(err){ + cb({ + headers: cb.header + ,body: Gun.text.ify({err: err || "Unknown error."}) + }); + }); + } + sub.s = {}; + sub.scribe = function(tab, id){ + sub.s[tab.sub] = tab; + tab.subs = tab.subs || {}; + tab.subs[id] = tab.subs[id] || gun.server.push.on(id).event(function(req){ + if(!req){ return } + if(!tab){ return this.off() } // resolve any dangling callbacks + req.sub = req.sub || req.headers['gun-sub']; + if(req.sub === tab.sub){ return } // do not send back to the tab that sent it + if(Gun.fns.is(tab.reply)){ + tab.reply({ + headers: {'Content-Type': sub.json, 'Gun-Sub': tab.sub} + ,body: Gun.text.ify(req.body) + }) + tab.reply = null; + return; + } + (tab.queue = tab.queue || []).push(req.body); + }); + } + sub.SLP = function(req, cb){ // Streaming Long Polling + //console.log("<-- ", req.sub, req.transport ," -->"); + req.tab = sub.s[req.sub]; + if(!req.tab){ + cb({ + headers: {'Content-Type': sub.json, 'Gun-Sub': ''} + ,body: Gun.text.ify({err: "Please re-initialize sub."}) + }); + return; + } + req.tab.sub = req.tab.sub || req.sub; + if(req.tab.queue && req.tab.queue.length){ + cb({ headers: {'Content-Type': sub.json, 'Gun-Sub': req.sub} }); + while(1 < req.tab.queue.length){ + cb({ chunk: Gun.text.ify(req.tab.queue.shift() + '\n') }); + } + cb({ body: Gun.text.ify(req.tab.queue.shift()) }); + } else { + req.tab.reply = cb; + } + } + sub.keyless = function(req, cb){ + cb({ + headers: {'Content-Type': sub.json} + ,body: {gun: true} + }); + } + sub.json = 'application/json'; + return sub; + }()); + opt.hook = opt.hook || {}; - gun.init({hook: { + gun.opt({hook: { load: opt.hook.load || s3.load ,set: opt.hook.set || s3.set ,key: opt.hook.key || s3.key + ,sub: opt.hook.sub || gun.server.sub }}, true); }); meta.json = 'application/json'; - meta.JSON = function(res, data){ - if(!res || res._headerSent){ return } - res.setHeader('Content-Type', meta.json); + meta.JSON = function(res, data, multi){ + if(res && !res._headerSent){ + res.setHeader('Content-Type', meta.json); + } + if(!data && multi){ + res.write(JSON.stringify(multi||'')+'\n'); + return; + } return res.end(JSON.stringify(data||'')); }; meta.CORS = function(req, res){