From b352cd742aca4f6eb276529f2b903daafe4f4e07 Mon Sep 17 00:00:00 2001 From: Jesse Gibson Date: Thu, 1 Dec 2016 16:51:35 -0700 Subject: [PATCH 1/6] Update client WebSocket plugin Lovely little abstraction layer over websockets sends messages using gun's new envelope system. Exponential backoff has not been implemented yet. If the socket fails to connect, it won't retry (yet). Also, this update left some dead code with the jsonp implementation. Soon that should be ported over to the new envelope system too, but now it's disconnected from gun. --- gun.js | 331 +++++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 226 insertions(+), 105 deletions(-) diff --git a/gun.js b/gun.js index 00c635e2..304d4659 100644 --- a/gun.js +++ b/gun.js @@ -932,7 +932,7 @@ return gun; } function output(at){ - var cat = this, gun = cat.gun, tmp; + var cat = this, gun = cat.gun, tmp; // TODO: BUG! Outgoing `get` to read from in memory!!! if(at.get && get(at, cat)){ return } //if(at.put){ @@ -1431,8 +1431,8 @@ return gun; } function output(at){ - var cat = this, gun = cat.gun, root = gun.Back(-1), put, get, tmp; - if(!at.gun){ + var cat = this, gun = cat.gun, root = gun.Back(-1), put, get, tmp; + if(!at.gun){ at.gun = gun; } if(at.get && !at.get[_soul]){ @@ -1746,7 +1746,7 @@ via: at, put: at.put[cat.get] } - + } else { if(obj_has(at.put, cat.get)){ return ev.off() } at = { @@ -2174,7 +2174,7 @@ gun.Back(-1).on('in', {'@': at['#']}); } return; - } + } if(Gun.obj.has(lex, '.')){var tmp = data[lex['.']];data = {_: data._};if(u !== tmp){data[lex['.']] = tmp}} //console.log('@@@@@@@@@@@@local get', data, at); gun.Back(-1).on('in', {'@': at['#'], put: Gun.graph.node(data)}); @@ -2184,83 +2184,223 @@ Gun.on('get', get); })(require, './adapters/localStorage'); - ;require(function(module){ - function r(base, body, cb, opt){ - var o = base.length? {base: base} : {}; - o.base = opt.base || base; - o.body = opt.body || body; - o.headers = opt.headers; - o.url = opt.url; - o.out = opt.out; - cb = cb || function(){}; - if(!o.base){ return } - r.transport(o, cb); - } - r.createServer = function(fn){ r.createServer.s.push(fn) } - r.createServer.ing = function(req, cb){ - var i = r.createServer.s.length; - while(i--){ (r.createServer.s[i] || function(){})(req, cb) } - } - r.createServer.s = []; - r.back = 2; r.backoff = 2; - r.transport = function(opt, cb){ - //Gun.log("TRANSPORT:", opt); - if(r.ws(opt, cb)){ return } - r.jsonp(opt, cb); - } - r.ws = function(opt, cb, req){ - var ws, WS = window.WebSocket || window.mozWebSocket || window.webkitWebSocket; - if(!WS){ return } - if(ws = r.ws.peers[opt.base]){ - req = req || {}; - if(opt.headers){ req.headers = opt.headers } - if(opt.body){ req.body = opt.body } - if(opt.url){ req.url = opt.url } - req.headers = req.headers || {}; - if(!opt.out && !ws.cbs[req.headers['ws-rid']]){ - ws.cbs[req.headers['ws-rid'] = 'WS' + (+ new Date()) + '.' + Math.floor((Math.random()*65535)+1)] = function(err,res){ - if(!res || res.body || res.end){ delete ws.cbs[req.headers['ws-rid']] } - cb(err,res); - } - } - if(!ws.readyState){ return setTimeout(function(){ r.ws(opt, cb, req) },100), true } - ws.sending = true; - ws.send(JSON.stringify(req)); - return true; - } - if(ws === false){ return } - (ws = r.ws.peers[opt.base] = new WS(opt.base.replace('http','ws'))).cbs = {}; - ws.onopen = function(o){ r.back = 2; r.ws(opt, cb) }; - ws.onclose = window.onbeforeunload = function(c){ - if(!ws || !c){ return } - if(ws.close instanceof Function){ ws.close() } - if(!ws.sending){ - ws = r.ws.peers[opt.base] = false; - return r.transport(opt, cb); - } - r.each(ws.cbs, function(cb){ - cb({err: "WebSocket disconnected!", code: !ws.sending? -1 : (ws||{}).err || c.code}); - }); - ws = r.ws.peers[opt.base] = null; // this will make the next request try to reconnect - setTimeout(function(){ // TODO: Have the driver handle this! - r.ws(opt, function(){}); // opt here is a race condition, is it not? Does this matter? - }, r.back *= r.backoff); - }; - ws.onmessage = function(m){ var res; - if(!m || !m.data){ return } - try{res = JSON.parse(m.data); - }catch(e){ return } - if(!res){ return } - res.headers = res.headers || {}; - if(res.headers['ws-rid']){ return (ws.cbs[res.headers['ws-rid']]||function(){})(null, res) } - if(res.body){ r.createServer.ing(res, function(res){ res.out = true; r(opt.base, null, null, res)}) } // emit extra events. - }; - ws.onerror = function(e){ (ws||{}).err = e }; - return true; - } - r.ws.peers = {}; - r.ws.cbs = {}; - r.jsonp = function(opt, cb){ + ;require(function(module){ + /* eslint-enable */ + /* eslint-env browser */ + /* eslint-disable require-jsdoc */ + /* eslint "comma-dangle": ["error", "never"] */ + var Gun = require('./gun'); + + function Client (url) { + if (!(this instanceof Client)) { + return new Client(url); + } + + this.url = Client.formatURL(url); + this.socket = null; + this.queue = []; + this.sid = Gun.text.random(10); + + this.on = Gun.on; + } + + Client.prototype = { + constructor: Client, + + drainQueue: function () { + var queue = this.queue; + var client = this; + + // Reset the queue. + this.queue = []; + + // Send each message. + queue.forEach(function (msg) { + client.send(msg); + }); + + return queue.length; + }, + + connect: function () { + var client = this; + var socket = new Client.WebSocket(this.url); + this.socket = socket; + + // Forward messages into the emitter. + socket.addEventListener('message', function (msg) { + client.on('message', msg); + }); + + // Send the messages in the queue. + this.ready(function () { + client.drainQueue(); + }); + + return socket; + }, + + isClosed: function () { + var socket = this.socket; + + if (!socket) { + return true; + } + + var state = socket.readyState; + + if (state === socket.CLOSING || state === socket.CLOSED) { + return true; + } + + return false; + }, + + ready: function (callback) { + var socket = this.socket; + var state = socket.readyState; + + if (state === socket.OPEN) { + callback(); + return; + } + + if (state === socket.CONNECTING) { + socket.addEventListener('open', callback); + } + }, + + send: function (msg) { + if (this.isClosed()) { + this.queue.push(msg); + + // Will send once connected. + this.connect(); + return false; + } + + var socket = this.socket; + + // Make sure the socket is open. + this.ready(function () { + socket.send(msg); + }); + + return true; + } + }; + + if (typeof window !== 'undefined') { + Client.WebSocket = window.WebSocket || + window.webkitWebSocket || + window.mozWebSocket || + null; + } + + Client.isSupported = Client.WebSocket !== null; + + // Ensure the protocol is correct. + Client.formatURL = function (url) { + return url.replace('http', 'ws'); + }; + + // Send a message to a group of peers. + Client.broadcast = function (urls, msg) { + var pool = Client.pool; + msg.headers = msg.headers || {}; + + Gun.obj.map(urls, function (options, addr) { + + var url = Client.formatURL(addr); + + var peer = pool[url]; + + var envelope = { + headers: Gun.obj.to(msg.headers, { + 'gun-sid': peer.sid + }), + body: msg.body + }; + + var serialized = Gun.text.ify(envelope); + + peer.send(serialized); + }); + + }; + + // A map of URLs to client instances. + Client.pool = {}; + + // Close all WebSockets when the window closes. + if (typeof window !== 'undefined') { + window.addEventListener('unload', function () { + Gun.obj.map(Client.pool, function (client) { + if (client.isClosed()) { + return; + } + + client.socket.close(); + }); + }); + } + + // Define client instances as gun needs them. + // Sockets will not be opened until absolutely necessary. + Gun.on('opt', function (ctx) { + var gun = ctx.gun; + var peers = gun.Back('opt.peers') || {}; + + Gun.obj.map(peers, function (options, addr) { + var url = Client.formatURL(addr); + + // Ignore clients we've seen before. + if (Client.pool.hasOwnProperty(url)) { + return; + } + + var client = Client.pool[url] = new Client(url); + + // Listen to incoming messages. + client.on('message', function (msg) { + var data; + + try { + data = Gun.obj.ify(msg.data); + } catch (err) { + // Invalid message, discard it. + return; + } + + if (!data || !data.body) { + return; + } + + gun.on('in', data); + }); + }); + }); + + // Broadcast the messages. + Gun.on('out', function (ctx) { + var gun = ctx.gun; + + var peers = gun.Back('opt.peers') || {}; + + // Validate. + if (Gun.obj.empty(peers) || !WebSocket) { + return; + } + + var msg = Gun.text.ify(ctx); + + if (Client.isSupported) { + Client.broadcast(peers, { body: msg }); + } + }); + + function r () {} + r.jsonp = function(opt, cb){ r.jsonp.ify(opt, function(url){ if(!url){ return } r.jsonp.send(url, function(err, reply){ @@ -2341,29 +2481,10 @@ } } module.exports = r; - })(require, './polyfill/request'); + /* eslint-disable */ + })(require, './polyfill/request'); - ;require(function(module){ - P.request = require('./request'); - function P(p){ - if(!P.is(this)){ return new P(p) } - this.peers = p; - } - P.is = function(p){ return (p instanceof P) } - P.chain = P.prototype; - function map(peer, url){ - var msg = this.msg; - var opt = this.opt || {}; - opt.out = true; - P.request(url, msg, null, opt); - } - P.chain.send = function(msg, opt){ - P.request.each(this.peers, map, {msg: msg, opt: opt}); - } - module.exports = P; - })(require, './polyfill/peer'); - - ;require(function(module){ + ;require(function(module){ if(typeof JSON === 'undefined'){ throw new Error("Include JSON first: ajax.cdnjs.com/ajax/libs/json2/20110223/json2.js") } // for old IE use if(typeof Gun === 'undefined'){ return } // TODO: window.Websocket is Browser only. But it would be nice if it could somehow merge it with lib/WSP? @@ -2486,4 +2607,4 @@ })(require, './adapters/wsp'); -}()); \ No newline at end of file +}()); From 01c93157cb0a89211c0eb10610dce7bbf131490e Mon Sep 17 00:00:00 2001 From: Jesse Gibson Date: Mon, 12 Dec 2016 14:37:09 -0700 Subject: [PATCH 2/6] Fix msg passing, add reconnect & backoff Fixed broadcast storm caused by re-enveloping the same message and re-emitting it (I passed the envelope to gun, not just the msg body). Sockets will attempt an exponential reconnect if the message queue isn't empty, otherwise they're still just lazy bums that only open when needed. JSONP wasn't working before I took on this project, but now it's completely disconnected. Once it's working, it should go in the `request` function as a websocket fallback. --- gun.js | 412 ++++++++++++++++++++++++++------------------------------- 1 file changed, 188 insertions(+), 224 deletions(-) diff --git a/gun.js b/gun.js index 304d4659..6bbd937e 100644 --- a/gun.js +++ b/gun.js @@ -1,6 +1,4 @@ -/* eslint-disable */ -/* eslint-enable no-console */ -//console.log("!!!!!!!!!!!!!!!! WARNING THIS IS GUN 0.5 !!!!!!!!!!!!!!!!!!!!!!"); +//console.log("!!!!!!!!!!!!!!!! WARNING THIS IS GUN 0.5 !!!!!!!!!!!!!!!!!!!!!!"); ;(function(){ /* UNBUILD */ @@ -121,7 +119,7 @@ t.r[k] = v; return; } t.r = t.r || []; - t.r.push(k); + t.r.push(k); }; Type.obj.map = function(l, c, _){ var u, i = 0, x, r, ll, lle, f = fn_is(c); @@ -1029,7 +1027,8 @@ Dedup.prototype.gc = function(){ var now = Gun.time.is(); var oldest = now; - var maxAge = 5 * 60 * 1000; + var maxAge = 5 * 60 * 1000; + var cache = this.cache; // TODO: Gun.scheduler already does this? Reuse that. Gun.obj.map(this.cache, function (time, id) { oldest = Math.min(now, time); @@ -1038,7 +1037,7 @@ return; } - delete this.cache[id]; + delete cache[id]; }); var done = Gun.obj.empty(this.cache); @@ -2185,15 +2184,19 @@ })(require, './adapters/localStorage'); ;require(function(module){ - /* eslint-enable */ - /* eslint-env browser */ - /* eslint-disable require-jsdoc */ - /* eslint "comma-dangle": ["error", "never"] */ var Gun = require('./gun'); - function Client (url) { + // Check for stone-age browsers. + if (typeof JSON === 'undefined') { + throw new Error( + 'Gun depends on JSON. Please load it first:\n' + + 'ajax.cdnjs.com/ajax/libs/json2/20110223/json2.js' + ); + } + + function Client (url, options) { if (!(this instanceof Client)) { - return new Client(url); + return new Client(url, options); } this.url = Client.formatURL(url); @@ -2202,6 +2205,9 @@ this.sid = Gun.text.random(10); this.on = Gun.on; + + this.options = options || {}; + this.resetBackoff(); } Client.prototype = { @@ -2232,6 +2238,15 @@ client.on('message', msg); }); + socket.addEventListener('close', function () { + + // Attempt reconnect if requests are pending. + if (client.queue.length) { + client.scheduleReconnect(); + } + + }); + // Send the messages in the queue. this.ready(function () { client.drainQueue(); @@ -2240,6 +2255,45 @@ return socket; }, + resetBackoff: function () { + var backoff = this.options; + + this.backoff = { + time: backoff.time || 100, + max: backoff.max || 30000, + factor: backoff.factor || 2 + }; + + return this.backoff; + }, + + nextBackoff: function () { + var backoff = this.backoff; + var next = backoff.time * backoff.factor; + var max = backoff.max; + + if (next > max) { + next = max; + } + + return (backoff.time = next); + }, + + // Try to efficiently reconnect. + scheduleReconnect: function () { + var client = this; + var time = this.backoff.time; + this.nextBackoff(); + + setTimeout(function () { + client.connect(); + + client.ready(function () { + client.resetBackoff(); + }); + }, time); + }, + isClosed: function () { var socket = this.socket; @@ -2348,6 +2402,7 @@ // Define client instances as gun needs them. // Sockets will not be opened until absolutely necessary. Gun.on('opt', function (ctx) { + var gun = ctx.gun; var peers = gun.Back('opt.peers') || {}; @@ -2376,11 +2431,17 @@ return; } - gun.on('in', data); + gun.on('in', data.body); }); }); }); + function request (peers, ctx) { + if (Client.isSupported) { + Client.broadcast(peers, { body: ctx }); + } + } + // Broadcast the messages. Gun.on('out', function (ctx) { var gun = ctx.gun; @@ -2392,219 +2453,122 @@ return; } - var msg = Gun.text.ify(ctx); - - if (Client.isSupported) { - Client.broadcast(peers, { body: msg }); - } + request(peers, ctx); }); - function r () {} - r.jsonp = function(opt, cb){ - r.jsonp.ify(opt, function(url){ - if(!url){ return } - r.jsonp.send(url, function(err, reply){ - cb(err, reply); - r.jsonp.poll(opt, reply); - }, opt.jsonp); - }); - } - r.jsonp.send = function(url, cb, id){ - var js = document.createElement('script'); - js.src = url; - js.onerror = function(c){ - (window[js.id]||function(){})(null, {err: "JSONP failed!"}); - } - window[js.id = id] = function(res, err){ - cb(err, res); - cb.id = js.id; - js.parentNode.removeChild(js); - window[cb.id] = null; // TODO: BUG: This needs to handle chunking! - try{delete window[cb.id]; - }catch(e){} - } - js.async = true; - document.getElementsByTagName('head')[0].appendChild(js); - return js; - } - r.jsonp.poll = function(opt, res){ - if(!opt || !opt.base || !res || !res.headers || !res.headers.poll){ return } - (r.jsonp.poll.s = r.jsonp.poll.s || {})[opt.base] = r.jsonp.poll.s[opt.base] || setTimeout(function(){ // TODO: Need to optimize for Chrome's 6 req limit? - //Gun.log("polling again"); - var o = {base: opt.base, headers: {pull: 1}}; - r.each(opt.headers, function(v,i){ o.headers[i] = v }) - r.jsonp(o, function(err, reply){ - delete r.jsonp.poll.s[opt.base]; - while(reply.body && reply.body.length && reply.body.shift){ // we're assuming an array rather than chunk encoding. :( - var res = reply.body.shift(); - if(res && res.body){ r.createServer.ing(res, function(){ r(opt.base, null, null, res) }) } // emit extra events. - } - }); - }, res.headers.poll); - } - r.jsonp.ify = function(opt, cb){ - var uri = encodeURIComponent, q = '?'; - if(opt.url && opt.url.pathname){ q = opt.url.pathname + q; } - q = opt.base + q; - r.each((opt.url||{}).query, function(v, i){ q += uri(i) + '=' + uri(v) + '&' }); - if(opt.headers){ q += uri('`') + '=' + uri(JSON.stringify(opt.headers)) + '&' } - if(r.jsonp.max < q.length){ return cb() } - q += uri('jsonp') + '=' + uri(opt.jsonp = 'P'+Math.floor((Math.random()*65535)+1)); - if(opt.body){ - q += '&'; - var w = opt.body, wls = function(w,l,s){ - return uri('%') + '=' + uri(w+'-'+(l||w)+'/'+(s||w)) + '&' + uri('$') + '='; + request.jsonp = function (opt, cb) { + request.jsonp.ify(opt, function (url) { + if (!url) { + return; + } + request.jsonp.send(url, function (err, reply) { + cb(err, reply); + request.jsonp.poll(opt, reply); + }, opt.jsonp); + }); + }; + request.jsonp.send = function (url, cb, id) { + var js = document.createElement('script'); + js.src = url; + js.onerror = function () { + (window[js.id] || function () {})(null, { + err: 'JSONP failed!' + }); + }; + window[js.id = id] = function (res, err) { + cb(err, res); + cb.id = js.id; + js.parentNode.removeChild(js); + delete window[cb.id]; + }; + js.async = true; + document.getElementsByTagName('head')[0].appendChild(js); + return js; + }; + request.jsonp.poll = function (opt, res) { + if (!opt || !opt.base || !res || !res.headers || !res.headers.poll) { + return; + } + var polls = request.jsonp.poll.s = request.jsonp.poll.s || {}; + polls[opt.base] = polls[opt.base] || setTimeout(function () { + var msg = { + base: opt.base, + headers: { pull: 1 } + }; + + request.each(opt.headers, function (header, name) { + msg.headers[name] = header; + }); + + request.jsonp(msg, function (err, reply) { + delete polls[opt.base]; + + var body = reply.body || []; + while (body.length && body.shift) { + var res = reply.body.shift(); + if (res && res.body) { + request.createServer.ing(res, function () { + request(opt.base, null, null, res); + }); + } + } + }); + }, res.headers.poll); + }; + request.jsonp.ify = function (opt, cb) { + var uri = encodeURIComponent, query = '?'; + if (opt.url && opt.url.pathname) { + query = opt.url.pathname + query; + } + query = opt.base + query; + request.each((opt.url || {}).query, function (value, key) { + query += (uri(key) + '=' + uri(value) + '&'); + }); + if (opt.headers) { + query += uri('`') + '=' + uri( + JSON.stringify(opt.headers) + ) + '&'; + } + if (request.jsonp.max < query.length) { + return cb(); + } + var random = Math.floor(Math.random() * (0xffff + 1)); + query += (uri('jsonp') + '=' + uri(opt.jsonp = 'P' + random)); + if (opt.body) { + query += '&'; + var w = opt.body, wls = function (w, l, s) { + return uri('%') + '=' + uri(w+'-'+(l||w)+'/'+(s||w)) + '&' + uri('$') + '='; } - if(typeof w != 'string'){ - w = JSON.stringify(w); - q += uri('^') + '=' + uri('json') + '&'; - } - w = uri(w); - var i = 0, l = w.length - , s = r.jsonp.max - (q.length + wls(l.toString()).length); - if(s < 0){ return cb() } - while(w){ - cb(q + wls(i, (i = i + s), l) + w.slice(0, i)); - w = w.slice(i); - } - } else { - cb(q); - } - } - r.jsonp.max = 2000; - r.each = function(obj, cb, as){ - if(!obj || !cb){ return } - for(var i in obj){ - if(obj.hasOwnProperty(i)){ - cb.call(as, obj[i], i); - } - } - } - module.exports = r; - /* eslint-disable */ + if (typeof w != 'string') { + w = JSON.stringify(w); + query += uri('^') + '=' + uri('json') + '&'; + } + w = uri(w); + var i = 0, l = w.length + , s = request.jsonp.max - (query.length + wls(l.toString()).length); + if (s < 0){ + return cb(); + } + while (w) { + cb(query + wls(i, (i += s), l) + w.slice(0, i)); + w = w.slice(i); + } + } else { + cb(query); + } + }; + request.jsonp.max = 2000; + request.each = function (obj, cb, as) { + if (!obj || !cb) { + return; + } + for (var key in obj) { + if (obj.hasOwnProperty(key)) { + cb.call(as, obj[key], key); + } + } + }; + module.exports = Client; })(require, './polyfill/request'); - - ;require(function(module){ - if(typeof JSON === 'undefined'){ throw new Error("Include JSON first: ajax.cdnjs.com/ajax/libs/json2/20110223/json2.js") } // for old IE use - if(typeof Gun === 'undefined'){ return } // TODO: window.Websocket is Browser only. But it would be nice if it could somehow merge it with lib/WSP? - - var root, noop = function(){}; - if(typeof window !== 'undefined'){ root = window } - - var Tab = {}; - Tab.on = Gun.on;//Gun.on.create(); - Tab.peers = require('../polyfill/peer'); - Gun.on('out', function(at){ - if(at.put){ return } // TODO: BUG! Doing this for now, to debug. However puts are handled below anyways, but it would be nice if we could switch over to this for both? - var gun = at.gun, opt = at.opt || {}, peers = opt.peers || gun.Back('opt.peers'); - if(!peers || Gun.obj.empty(peers)){ - Gun.log.once('peers', "Warning! You have no peers to connect to!"); - return; - } - var msg = at; - /* - var msg = { - '#': at['#'] || Gun.text.random(9), // msg ID - '$': at.get // msg BODY - }; - */ - Tab.on(msg['#'], function(err, data){ // TODO: ONE? PERF! Clear out listeners, maybe with setTimeout? - if(data){ - at.gun.Back(-1).on('out', {'@': at['#'], err: err, put: data}); - } else { - at.gun.Back(-1).on('in', {'@': at['#'], err: err, put: data}); - } - }); - Tab.peers(peers).send(msg, {headers: {'gun-sid': Tab.server.sid}}); - }); - Gun.on('put', function(at){ - if(at['@']){ return } - var opt = at.gun.Back('opt') || {}, peers = opt.peers; - if(!peers || Gun.obj.empty(peers)){ - Gun.log.once('peers', "Warning! You have no peers to save to!"); - return; - } - if(false === opt.websocket || (at.opt && false === at.opt.websocket)){ return } - var msg = at || { - '#': at['#'] || Gun.text.random(9), // msg ID - '$': at.put // msg BODY - }; - Tab.on(msg['#'], function(err, ok){ // TODO: ONE? PERF! Clear out listeners, maybe with setTimeout? - at.gun.Back(-1).on('in', {'@': at['#'], err: err, ok: ok}); - }); - Tab.peers(peers).send(msg, {headers: {'gun-sid': Tab.server.sid}}); - }); - // browser/client side Server! - Gun.on('opt', function(at){ // TODO: BUG! Does not respect separate instances!!! - if(Tab.server){ return } - var gun = at.gun, server = Tab.server = {}, tmp; - server.sid = Gun.text.random(); - Tab.peers.request.createServer(function(req, res){ - if(!req || !res || !req.body || !req.headers){ return } - var msg = req.body; - gun.on('in', req.body); - return; - // AUTH for non-replies. - if(server.msg(msg['#'])){ return } - //server.on('network', Gun.obj.copy(req)); // Unless we have WebRTC, not needed. - if(msg['@']){ // no need to process. - if(Tab.ons[tmp = msg['@'] || msg['#']]){ - Tab.on(tmp, [msg['!'], msg['$']]); - } - return - } - if(msg['$'] && msg['$']['#']){ return server.get(req, res) } - else { return server.put(req, res) } - }); - server.get = function(req, cb){ - var body = req.body, lex = body['$'], opt; - var graph = gun._.root._.graph, node; - if(!(node = graph[lex['#']])){ return } // Don't reply to data we don't have it in memory. TODO: Add localStorage? - cb({body: { - '#': server.msg(), - '@': body['#'], - '$': node - }}); - } - server.put = function(req, cb){ - var body = req.body, graph = body['$']; - var __ = gun._.root._; - if(!(graph = Gun.obj.map(graph, function(node, soul, map){ // filter out what we don't have in memory. - if(!__.path[soul]){ return } - map(soul, node); - }))){ return } - gun.on('out', {gun: gun, opt: {websocket: false}, put: graph, '#': Gun.on.ask(function(ack, ev){ - if(!ack){ return } - ev.off(); - return cb({body: { - '#': server.msg(), - '@': body['#'], - '$': ack, - '!': ack.err - }}); - })}); - } - server.msg = function(id){ - if(!id){ - return server.msg.debounce[id = Gun.text.random(9)] = Gun.time.is(), id; - } - clearTimeout(server.msg.clear); - server.msg.clear = setTimeout(function(){ - var now = Gun.time.is(); - Gun.obj.map(server.msg.debounce, function(t,id){ - if((now - t) < (1000 * 60 * 5)){ return } - Gun.obj.del(server.msg.debounce, id); - }); - },500); - if(server.msg.debounce[id]){ - return server.msg.debounce[id] = Gun.time.is(), id; - } - server.msg.debounce[id] = Gun.time.is(); - return; - }; - server.msg.debounce = server.msg.debounce || {}; - }); - - })(require, './adapters/wsp'); - + }()); From 30fe844cab5168a3097445136f927868c783fc95 Mon Sep 17 00:00:00 2001 From: Jesse Gibson Date: Mon, 12 Dec 2016 16:10:39 -0700 Subject: [PATCH 3/6] Check JSON support just once Two separate modules (localStorage & websockets) ensured JSON was globally defined. Since only one is needed to alert the user, the other has been removed. --- gun.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/gun.js b/gun.js index efea12bf..18aba562 100644 --- a/gun.js +++ b/gun.js @@ -2142,8 +2142,7 @@ })(require, './api'); ;require(function(module){ - if(typeof JSON === 'undefined'){ throw new Error("Include JSON first: ajax.cdnjs.com/ajax/libs/json2/20110223/json2.js") } // for old IE use - if(typeof Gun === 'undefined'){ return } // TODO: localStorage is Browser only. But it would be nice if it could somehow plugin into NodeJS compatible localStorage APIs? + if(typeof Gun === 'undefined'){ return } // TODO: localStorage is Browser only. But it would be nice if it could somehow plugin into NodeJS compatible localStorage APIs? var root, noop = function(){}; if(typeof window !== 'undefined'){ root = window } @@ -2448,7 +2447,7 @@ var peers = gun.Back('opt.peers') || {}; // Validate. - if (Gun.obj.empty(peers) || !WebSocket) { + if (Gun.obj.empty(peers)) { return; } From 304265e193ed3506eb499382dc4ecaaaec3523d3 Mon Sep 17 00:00:00 2001 From: Jesse Gibson Date: Mon, 12 Dec 2016 16:24:51 -0700 Subject: [PATCH 4/6] Pass backoff options from constructor The Client constructor supports backoff configuration. This commit exposes it to the end developer via the `peers` options object, in the `backoff` property. Example: Gun({ peers: { 'url.com/gun': { backoff: { max: 1000, factor: 0.2 } } } }) --- gun.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/gun.js b/gun.js index 18aba562..b8b6c08e 100644 --- a/gun.js +++ b/gun.js @@ -2412,7 +2412,10 @@ return; } - var client = Client.pool[url] = new Client(url); + var client = new Client(url, options.backoff); + + // Add it to the pool. + Client.pool[url] = client; // Listen to incoming messages. client.on('message', function (msg) { From ba673f73f8f4bf9177beef01d1138cb02b047add Mon Sep 17 00:00:00 2001 From: Jesse Gibson Date: Tue, 13 Dec 2016 13:37:43 -0700 Subject: [PATCH 5/6] Attempt reconnect on disconnect Previously, the client websocket layer would only attempt a reconnection if there were pending messages in the queue. Now it will always attempt a reconnection, solving most of issue #259 (although not completely, as reconnects are susceptible to race conditions). --- gun.js | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/gun.js b/gun.js index b8b6c08e..b47a1e6f 100644 --- a/gun.js +++ b/gun.js @@ -1,5 +1,5 @@ //console.log("!!!!!!!!!!!!!!!! WARNING THIS IS GUN 0.5 !!!!!!!!!!!!!!!!!!!!!!"); -;(function(){ +;(function(){ /* UNBUILD */ var root; @@ -2236,13 +2236,9 @@ client.on('message', msg); }); + // Reconnect on close events. socket.addEventListener('close', function () { - - // Attempt reconnect if requests are pending. - if (client.queue.length) { - client.scheduleReconnect(); - } - + client.scheduleReconnect(); }); // Send the messages in the queue. From a566572a2d96f5dfb76a9042f7975899504a106c Mon Sep 17 00:00:00 2001 From: Jesse Gibson Date: Thu, 15 Dec 2016 15:35:11 -0700 Subject: [PATCH 6/6] Shallow copy constructor options Previously constructor options were deeply copied, which caused issues with 3rd party plugins when they pass "class" instances as options (since instances were copied into POJOs, breaking the prototype chain). Now it's shallow copied. Special treatment is still given for the `peers` property. --- gun.js | 54 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 30 insertions(+), 24 deletions(-) diff --git a/gun.js b/gun.js index b47a1e6f..2eeb9634 100644 --- a/gun.js +++ b/gun.js @@ -903,32 +903,38 @@ ;(function(){ - Gun.chain.opt = function(opt){ - opt = opt || {}; - var gun = this, at = gun._, tmp, u; - at.root = at.root || gun; + Gun.chain.opt = function(opt){ + opt = opt || {}; + var peers = obj_is(opt) ? opt.peers : opt; + if (text_is(peers)) { + peers = [peers]; + } + if (list_is(peers)) { + peers = obj_map(peers, function (url, field, m) { + m(url, {}); + }); + } + if (!obj_is(opt)) { + opt = {}; + } + opt.peers = peers; + var gun = this, at = gun._; + at.root = at.root || gun; at.graph = at.graph || {}; at.dedup = new Dedup(); - at.opt = at.opt || {}; - if(text_is(opt)){ opt = {peers: opt} } - else if(list_is(opt)){ opt = {peers: opt} } - if(text_is(opt.peers)){ opt.peers = [opt.peers] } - if(list_is(opt.peers)){ opt.peers = obj_map(opt.peers, function(n,f,m){m(n,{})}) } - obj_map(opt, function map(v,f){ - if(obj_is(v)){ - obj_map(v, map, this[f] || (this[f] = {})); // TODO: Bug? Be careful of falsey values getting overwritten? - return; - } - this[f] = v; - }, at.opt); - Gun.on('opt', at); - if(!at.once){ - gun.on('in', input, at); - gun.on('out', output, at); - } - at.once = true; - return gun; - } + at.opt = at.opt || {}; + + at.opt.peers = Gun.obj.to(at.opt.peers || {}, peers); + Gun.obj.to(opt, at.opt); + + Gun.on('opt', at); + if(!at.once){ + gun.on('in', input, at); + gun.on('out', output, at); + } + at.once = true; + return gun; + } function output(at){ var cat = this, gun = cat.gun, tmp; // TODO: BUG! Outgoing `get` to read from in memory!!!