diff --git a/.gitignore b/.gitignore index 2cbe713e..56f45cc2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ node_modules/* npm-debug.log +gun.min.js yarn.lock *data.json *.db diff --git a/Dockerfile b/Dockerfile index 09650cd7..2fe0ef39 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,3 +1,24 @@ -FROM node:6-onbuild +FROM alpine:edge +# Build-time metadata as defined at http://label-schema.org +ARG BUILD_DATE +ARG VCS_REF +ARG VCS_URL +ARG VERSION +LABEL org.label-schema.build-date=$BUILD_DATE \ + org.label-schema.name="Gun - Offline First, Javascript Graph Database" \ +# org.label-schema.description="Let it be pulled from Readme.md..." \ + org.label-schema.url="http://gun.js.org" \ + org.label-schema.vcs-ref=$VCS_REF \ + org.label-schema.vcs-url=$VCS_URL \ + org.label-schema.vendor="The Gun Database Team" \ + org.label-schema.version=$VERSION \ + org.label-schema.schema-version="1.0" +WORKDIR /app +ADD . . ENV NPM_CONFIG_LOGLEVEL warn +RUN apk update && apk upgrade \ + && apk add --no-cache ca-certificates nodejs \ + && npm install \ + && rm -rf /var/cache/* -rf /tmp/npm* EXPOSE 8080 +CMD ["npm","start"] diff --git a/README.md b/README.md index 7449fb4d..64d47571 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,12 @@ -# gun [![NPM downloads](https://img.shields.io/npm/dm/gun.svg?style=flat)](https://npmjs.org/package/gun) [![Build Status](https://travis-ci.org/amark/gun.svg?branch=master)](https://travis-ci.org/amark/gun) [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/amark/gun?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +

+ + gun + +

+[![NPM downloads](https://img.shields.io/npm/dm/gun.svg?style=flat)](https://npmjs.org/package/gun) [![Build Status](https://travis-ci.org/amark/gun.svg?branch=master)](https://travis-ci.org/amark/gun) [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/amark/gun?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +[![Docker Automated buil](https://img.shields.io/docker/automated/gundb/gun.svg)](https://hub.docker.com/r/gundb/gun/) [![](https://images.microbadger.com/badges/image/gundb/gun.svg)](https://microbadger.com/images/gundb/gun "Get your own image badge on microbadger.com") [![Docker Pulls](https://img.shields.io/docker/pulls/gundb/gun.svg)](https://hub.docker.com/r/gundb/gun/) [![Docker Stars](https://img.shields.io/docker/stars/gundb/gun.svg)](https://hub.docker.com/r/gundb/gun/) GUN is a realtime, distributed, offline-first, graph database engine. Lightweight and powerful, at just **~9KB** gzipped. @@ -47,16 +55,31 @@ Try the [interactive tutorial](http://gun.js.org/think.html) in the browser (**5 - To quickly spin up a Gun test server for your development team, uilize eiher [Heroku](http://heroku.com) or [Docker](http://docker.com) or any variant thereof ([Dokku](http://dokku.viewdocs.io/dokku/), [Flynn.io](http://flynn.io), [now.sh](https://zeit.co/now), etc) -### Docker - ```bash +### [Docker](https://www.docker.com/) + - Either (fastest) from the [Docker Hub](https://hub.docker.com/r/gundb/gun/)(Built at [![](https://images.microbadger.com/badges/commit/gundb/gun.svg)](https://microbadger.com/images/gundb/gun "Get your own commit badge on microbadger.com")): + +```bash + docker run -p 8080:8080 gundb/gun +``` + - Or build the [Docker](https://docs.docker.com/engine/installation/) image locally: + +```bash git clone https://github.com/amark/gun.git cd gun docker build -t myrepo/gundb:v1 . docker run -p 8080:8080 myrepo/gundb:v1 - ``` +``` + - Or, if you prefer your Docker image with metadata labels (Linux/Mac only): + + ```bash + npm run docker + docker run -p 8080:8080 usenameHere/gun:git +``` Then visit [http://localhost:8080](http://localhost:8080) in your browser. -### Hiroku +### [Heroku](https://www.heroku.com/) + - Either click [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/amark/gun/tree/0.5) to deploy to your existing Heroku account immediately, OR: + ```bash git clone https://github.com/amark/gun.git cd gun @@ -65,7 +88,7 @@ Try the [interactive tutorial](http://gun.js.org/think.html) in the browser (**5 ``` Then visit the URL in the output of the 'heroku create' step, in your browser. -### Now.sh +### [Now.sh](https://zeit.co/now/) ```bash npm install -g now git clone https://github.com/amark/gun.git @@ -104,7 +127,7 @@ Designed with ♥ by Mark Nadal, the gun team, and many amazing contributors. L Thanks to the following people who have contributed to GUN, via code, issues, or conversation (this list has quickly become tremendously behind! We'll probably turn this into a dedicated wiki page so you can add yourself): -[agborkowski](https://github.com/agborkowski); [alexlafroscia](https://github.com/alexlafroscia); [anubiann00b](https://github.com/anubiann00b); [bromagosa](https://github.com/bromagosa); [coolaj86](https://github.com/coolaj86); [d-oliveros](https://github.com/d-oliveros), [danscan](https://github.com/danscan); **[forrestjt](https://github.com/forrestjt) ([file.js](https://github.com/amark/gun/blob/master/lib/file.js))**; [gedw99](https://github.com/gedw99); [HelloCodeMing](https://github.com/HelloCodeMing); **[JosePedroDias](https://github.com/josepedrodias) (graph visualizer)**; **[jveres](https://github.com/jveres) ([todoMVC](https://github.com/jveres/todomvc) [live demo](http://todos.loqali.com/))**; [ndarilek](https://github.com/ndarilek); [onetom](https://github.com/onetom); [phpnode](https://github.com/phpnode); [PsychoLlama](https://github.com/PsychoLlama); **[RangerMauve](https://github.com/RangerMauve) ([schema](https://github.com/gundb/gun-schema))**; [riston](https://github.com/riston); [rootsical](https://github.com/rootsical); [rrrene](https://github.com/rrrene); [ssr1ram](https://github.com/ssr1ram); [Xe](https://github.com/Xe); [zot](https://github.com/zot); +[agborkowski](https://github.com/agborkowski); [alexlafroscia](https://github.com/alexlafroscia); [anubiann00b](https://github.com/anubiann00b); [bromagosa](https://github.com/bromagosa); [coolaj86](https://github.com/coolaj86); [d-oliveros](https://github.com/d-oliveros), [danscan](https://github.com/danscan); **[forrestjt](https://github.com/forrestjt) ([file.js](https://github.com/amark/gun/blob/master/lib/file.js))**; [gedw99](https://github.com/gedw99); [HelloCodeMing](https://github.com/HelloCodeMing); **[Hillct](https://github.com/hillct) (Deployment Tools); **[JosePedroDias](https://github.com/josepedrodias) (graph visualizer)**; **[jveres](https://github.com/jveres) ([todoMVC](https://github.com/jveres/todomvc) [live demo](http://todos.loqali.com/))**; [ndarilek](https://github.com/ndarilek); [onetom](https://github.com/onetom); [phpnode](https://github.com/phpnode); [PsychoLlama](https://github.com/PsychoLlama); **[RangerMauve](https://github.com/RangerMauve) ([schema](https://github.com/gundb/gun-schema))**; [riston](https://github.com/riston); [rootsical](https://github.com/rootsical); [rrrene](https://github.com/rrrene); [ssr1ram](https://github.com/ssr1ram); [Xe](https://github.com/Xe); [zot](https://github.com/zot); [ayurmedia](https://github.com/ayurmedia); This list of contributors was manually compiled and alphabetically sorted. If we missed you, please submit an issue so we can get you added! diff --git a/app.json b/app.json new file mode 100644 index 00000000..005f6f0e --- /dev/null +++ b/app.json @@ -0,0 +1,8 @@ +{ + "name": "gun-server", + "website": "http://gun.js.org", + "repository": "https://github.com/amark/gun", + "logo": "https://avatars3.githubusercontent.com/u/8811914", + "keywords": ["node", "gun", "gunDB", "database","graph","offline-first"], + "description": "Javascript, Offline-First Javascript Graph Database Server Peer" +} diff --git a/gun.js b/gun.js index 4bb38809..71895597 100644 --- a/gun.js +++ b/gun.js @@ -1,3 +1,5 @@ +/* eslint-disable */ +/* eslint-enable no-console */ //console.log("!!!!!!!!!!!!!!!! WARNING THIS IS GUN 0.5 !!!!!!!!!!!!!!!!!!!!!!"); ;(function(){ @@ -74,7 +76,7 @@ Type.list.map = function(l, c, _){ return obj_map(l, c, _) } Type.list.index = 1; // change this to 0 if you want non-logical, non-mathematical, non-matrix, non-convenient array notation Type.obj = {is: function(o){ return o? (o instanceof Object && o.constructor === Object) || Object.prototype.toString.call(o).match(/^\[object (\w+)\]$/)[1] === 'Object' : false }} - Type.obj.put = function(o, f, v){ return (o||{})[f] = v, o } + Type.obj.put = function(o, f, v){ return (o||{})[f] = v, o } Type.obj.has = function(o, f){ return o && Object.prototype.hasOwnProperty.call(o, f) } Type.obj.del = function(o, k){ if(!o){ return } @@ -165,7 +167,7 @@ var obj = Type.obj, obj_is = obj.is, obj_has = obj.has, obj_map = obj.map; module.exports = Type; })(require, './type'); - + ;require(function(module){ // On event emitter generic javascript utility. function Scope(){ @@ -291,7 +293,7 @@ ;require(function(module){ // TODO: Needs to be redone. var On = require('./on'); - + function Chain(create, opt){ opt = opt || {}; opt.id = opt.id || '#'; @@ -357,6 +359,7 @@ on.ack = function(at, reply){ if(!at || !reply || !ask.on){ return } var id = at[opt.id] || at; + if(!ask.ons[id]){ return } ask.on(id, reply); return true; } @@ -479,11 +482,11 @@ } if(incomingState < currentState){ return {historical: true}; // the incoming value is within the boundary of the machine's state, but not within the range. - + } if(currentState < incomingState){ return {converge: true, incoming: true}; // the incoming value is within both the boundary and the range of the machine's state. - + } if(incomingState === currentState){ if(incomingValue === currentValue){ // Note: while these are practically the same, the deltas could be technically different @@ -623,7 +626,7 @@ if(o.node){ o.node[f] = tmp } return; } - if(Val.is(v)){ + if(Val.is(v)){ o.node[f] = v; } } @@ -708,8 +711,8 @@ } function map(n, s){ // we invert this because the way we check for this is via a negation. if(!n || s !== Node.soul(n) || !Node.is(n, this.fn)){ return true } // it is true that this is an invalid graph. - if(!fn_is(this.cb)){ return } - nf.n = n; nf.as = this.as; + if(!fn_is(this.cb)){ return } + nf.n = n; nf.as = this.as; this.cb.call(nf.as, n, s, nf); } }()); @@ -718,7 +721,7 @@ var at = {path: [], obj: obj}; if(!env){ env = {}; - } else + } else if(typeof env === 'string'){ env = {soul: env}; } else @@ -848,6 +851,7 @@ ;require(function(module){ function Gun(o){ + if(o instanceof Gun){ return this } if(!(this instanceof Gun)){ return Gun.create(o) } this._ = {gun: this}; } @@ -896,7 +900,9 @@ Gun.chain.opt = function(opt){ opt = opt || {}; var gun = this, at = gun._, tmp, u; - if(!at.root){ root(at) } + 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} } @@ -910,28 +916,35 @@ 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; } - function root(at){ - var gun = at.gun; - at.root = gun; - at.graph = {}; - gun.on('in', input, at); - gun.on('out', output, at); - } function output(at){ var cat = this, gun = cat.gun, tmp; - if(at.put){ - cat.on('in', obj_to(at, {'#': 0, gun: cat.gun})); + // TODO: BUG! Outgoing `get` to read from in memory!!! + if(at.get && get(at, cat)){ return } + //if(at.put){ + cat.on('in', obj_to(at, {gun: cat.gun})); // TODO: PERF! input now goes to output so it would be nice to reduce the circularity here for perf purposes. + //} + if(at['#']){ + cat.dedup.track(at['#']); } if(!at.gun){ at = Gun.obj.to(at, {gun: gun}); } - if(at.put){ Gun.on('put', at) } - if(at.get){ get(at, cat) } - Gun.on('out', at); - if(!cat.back){ return } - cat.back.on('out', at); + //if(at.put){ Gun.on('put', at) } + //if(at.get){ get(at, cat) } + // Reads and writes both trigger output. // that should be intended. + //if (at.put !== undefined || at.get !== undefined) { + Gun.on('out', at); + //} + // Gun.on('out', at); + //if(!cat.back){ return } + //cat.back.on('out', at); } function get(at, cat){ var soul = at.get[_soul], node = cat.graph[soul], field = at.get[_field]; @@ -941,22 +954,34 @@ } cat.on('in', { '@': at['#'], - put: Gun.graph.node(node), // TODO: BUG! Clone node! + put: Gun.graph.node(node) // TODO: BUG! Clone node! }); - return; + return true; } - Gun.on('get', at); + //Gun.on('get', at); } function input(at){ var cat = this; - if(at.err || u === at.put){ - at.gun = at.gun || cat.gun; + if(!at.gun){ at.gun = cat.gun } + if(!at['#'] && at['@']){ + at['#'] = Gun.text.random(); // TODO: Use what is used other places instead. Gun.on.ack(at['@'], at); + cat.dedup.track(at['#']); + cat.on('out', at); return; } - if(cat.graph){ - Gun.obj.map(at.put, ham, {at: at, cat: cat}); // all unions must happen first, sadly. + if(at['#'] && cat.dedup.check(at['#'])){ return } + cat.dedup.track(at['#']); + Gun.on.ack(at['@'], at); + if(at.put){ + if(cat.graph){ + Gun.obj.map(at.put, ham, {at: at, cat: this}); // all unions must happen first, sadly. + } + Gun.obj.map(at.put, map, {at: at, cat: this}); + //if(0 === at['@']){ return } // TODO: UNCLEAN! Temporary hack for now. + Gun.on('put', at); } - Gun.obj.map(at.put, map, {at: at, cat: cat}); + if(at.get){ Gun.on('get', at) } + Gun.on('out', at); } function ham(data, key){ var cat = this.cat, graph = cat.graph; @@ -978,6 +1003,54 @@ via: this.at }); } + function Dedup(){ + this.cache = {}; + } + Dedup.prototype.track = function (id) { + this.cache[id] = Gun.time.is(); + // Engage GC. + if (!this.to) { + this.gc(); + } + return id; + }; + Dedup.prototype.check = function(id){ + // Have we seen this ID recently? + return Gun.obj.has(this.cache, id); + } + Dedup.prototype.gc = function(){ + var now = Gun.time.is(); + var oldest = now; + var maxAge = 5 * 60 * 1000; + // TODO: Gun.scheduler already does this? Reuse that. + Gun.obj.map(this.cache, function (time, id) { + oldest = Math.min(now, time); + + if ((now - time) < maxAge) { + return; + } + + delete this.cache[id]; + }); + + var done = Gun.obj.empty(this.cache); + + // Disengage GC. + if (done) { + this.to = null; + return; + } + + // Just how old? + var elapsed = now - oldest; + + // How long before it's too old? + var nextGC = maxAge - elapsed; + + // Schedule the next GC event. + var dedup = this; + this.to = setTimeout(function(){ dedup.gc() }, nextGC); + } }()); var text = Type.text, text_is = text.is, text_random = text.random; var list = Type.list, list_is = list.is; @@ -1011,7 +1084,7 @@ var is = state_is(node, field), cs = state_is(vertex, field); if(u === is || u === cs){ return true } // it is true that this is an invalid HAM comparison. var iv = rel_is(value) || value, cv = rel_is(vertex[field]) || vertex[field]; - + @@ -1081,7 +1154,7 @@ var obj = Gun.obj, obj_is = obj.is, obj_put = obj.put, obj_map = obj.map, obj_empty = obj.empty; var num = Gun.num, num_is = num.is; var _soul = Gun.val.rel._, _field = '.'; - + ;(function(){ var obj = {}, u; Gun.chain.Back = function(n, opt){ var tmp; if(-1 === n || Infinity === n){ @@ -1211,7 +1284,7 @@ as.batch(); } - function any(at, ev){ + function any(at, ev){ function implicit(at){ // TODO: CLEAN UP!!!!! if(!at || !at.get){ return } // TODO: CLEAN UP!!!!! as.data = obj_put({}, tmp = at.get, as.data); // TODO: CLEAN UP!!!!! @@ -1222,9 +1295,9 @@ implicit(at); // TODO: CLEAN UP!!!!! } // TODO: CLEAN UP!!!!! var as = this; - if(at.err){ + if(at.err){ console.log("Please report this as an issue! Put.any.err"); - return + return } var cat = as.ref._, data = at.put, opt = as.opt, root, tmp; if(u === data){ @@ -1309,7 +1382,7 @@ Gun.chain.chain = function(){ var chain = new this.constructor(), _; - _ = chain._ || (chain._ = {}); + _ = chain._ || (chain._ = {gun: chain}); _.root = this._.root; _.back = this; Gun.on('chain', _); @@ -1365,6 +1438,8 @@ var cat = back._, next = cat.next, gun = back.chain(), at = gun._; if(!next){ next = cat.next = {} } next[at.get = key] = gun; + if(cat.root === cat){ at.soul = key } + else if(cat.soul || cat.field){ at.field = key } return gun; } function output(at){ @@ -1390,7 +1465,7 @@ if(u === val){ at.gun.on('in', { get: get, - gun: at.gun, + gun: at.gun, via: tac }); return; @@ -1675,6 +1750,37 @@ //at.get = at.get || cat.get; cat.on('in', at); } + + function ackk(at, ev){ var gun = this.gun; + var cat = gun._; + if(u !== cat.change){ return ev.off() } + // TODO: PERF! Memory. If somebody `gun.off()` we should clean up these requests. + // TODO: PERF! Memory. If peers only reply with `not` (or we never get replies) these event listeners will be left hanging - even if we get push updates that the data does exist. + if(cat.root === cat.back){ + //at.gun = cat.gun; + if(at.gun === cat.gun){ return } + at = { + get: cat.get, + gun: cat.gun, + via: at, + put: at.put[cat.get] + } + + } else { + if(obj_has(at.put, cat.get)){ return ev.off() } + at = { + get: cat.get, + gun: gun, + via: at.via? at : { + get: cat.back._.get, + gun: cat.back, + via: at + } + } + } + //at.get = at.get || cat.get; + cat.on('in', at); + } var obj = Gun.obj, obj_has = obj.has, obj_to = obj.to; var empty = {}, u; var _soul = Gun._.soul, _field = Gun._.field, node_ = Gun.node._, _sid = Gun.on.ask._, _rid = Gun.on.ack._; @@ -2057,20 +2163,22 @@ ;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? - + var root, noop = function(){}; if(typeof window !== 'undefined'){ root = window } var store = root.localStorage || {setItem: noop, removeItem: noop, getItem: noop}; function put(at){ var err, id, opt, root = at.gun._.root; - (opt = at.opt || {}).prefix = opt.prefix || at.gun.Back('opt.prefix') || 'gun/'; + (opt = {}).prefix = (at.opt || opt).prefix || at.gun.Back('opt.prefix') || 'gun/'; Gun.graph.is(at.put, function(node, soul){ //try{store.setItem(opt.prefix + soul, Gun.text.ify(node)); try{store.setItem(opt.prefix + soul, Gun.text.ify(root._.graph[soul]||node)); }catch(e){ err = e || "localStorage failure" } }); //console.log('@@@@@@@@@@local put!'); - Gun.on.ack(at, {err: err, ok: 0}); // TODO: Reliability! Are we sure we want to have localStorage ack? + if(Gun.obj.empty(at.gun.Back('opt.peers'))){ + Gun.on.ack(at, {err: err, ok: 0}); // only ack if there are no peers. + } } function get(at){ var gun = at.gun, lex = at.get, soul, data, opt, u; @@ -2078,7 +2186,12 @@ (opt = at.opt || {}).prefix = opt.prefix || at.gun.Back('opt.prefix') || 'gun/'; if(!lex || !(soul = lex[Gun._.soul])){ return } data = Gun.obj.ify(store.getItem(opt.prefix + soul) || null); - if(!data){ return } // localStorage isn't trustworthy to say "not found". + if(!data){ // localStorage isn't trustworthy to say "not found". + if(Gun.obj.empty(gun.Back('opt.peers'))){ + 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)}); @@ -2087,7 +2200,7 @@ Gun.on('put', put); Gun.on('get', get); })(require, './adapters/localStorage'); - + ;require(function(module){ function r(base, body, cb, opt){ var o = base.length? {base: base} : {}; @@ -2270,26 +2383,27 @@ ;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('get', function(at){ + 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)){ - //setTimeout(function(){ Gun.log.once('peers', "Warning! You have no peers to connect to!"); - at.gun.Back(-1).on('in', {'@': at['#']}); - //},100); 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}); @@ -2304,11 +2418,10 @@ 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!"); - at.gun.Back(-1).on('in', {'@': at['#']}); return; } if(false === opt.websocket || (at.opt && false === at.opt.websocket)){ return } - var msg = { + var msg = at || { '#': at['#'] || Gun.text.random(9), // msg ID '$': at.put // msg BODY }; @@ -2325,6 +2438,8 @@ 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. @@ -2332,7 +2447,7 @@ if(Tab.ons[tmp = msg['@'] || msg['#']]){ Tab.on(tmp, [msg['!'], msg['$']]); } - return + return } if(msg['$'] && msg['$']['#']){ return server.get(req, res) } else { return server.put(req, res) } @@ -2377,12 +2492,12 @@ Gun.obj.del(server.msg.debounce, id); }); },500); - if(server.msg.debounce[id]){ + 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 || {}; }); diff --git a/hooks/build b/hooks/build new file mode 100755 index 00000000..1f6fd415 --- /dev/null +++ b/hooks/build @@ -0,0 +1,7 @@ +#!/bin/bash +FOO=${IMAGE_NAME:=`whoami`/gun:git-local} +BAR=${SOURCE_BRANCH:=`git rev-parse --abbrev-ref HEAD`} +docker build --build-arg BUILD_DATE=`date -u +"%Y-%m-%dT%H:%M:%SZ"` \ + --build-arg VCS_REF=`git rev-parse --short HEAD` \ + --build-arg VCS_URL=`git config --get remote.origin.url` \ + --build-arg VERSION=$SOURCE_BRANCH -t $IMAGE_NAME . diff --git a/hooks/post_push b/hooks/post_push new file mode 100755 index 00000000..38af73a6 --- /dev/null +++ b/hooks/post_push @@ -0,0 +1,3 @@ +#!/bin/bash +docker tag $IMAGE_NAME $DOCKER_REPO:latest +docker push $DOCKER_REPO:latest diff --git a/lib/wsp/Peer.js b/lib/wsp/Peer.js index 05f510b3..493ba8e7 100644 --- a/lib/wsp/Peer.js +++ b/lib/wsp/Peer.js @@ -1,10 +1,16 @@ +/* eslint-disable no-underscore-dangle */ +'use strict'; + var WebSocket = require('ws'); +var Emitter = require('events'); +var util = require('util'); /** * Calculates backoff instances. * @param {Object} [options] - Override the default settings. * @param {Object} options.time=50 - Initial backoff time. * @param {Object} options.factor=2 - How much to multiply the time by. + * @param {Object} options.max=1min - Maximum backoff time. * @class */ function Backoff (options) { @@ -19,7 +25,14 @@ function Backoff (options) { * @return {Number} - The next backoff time. */ Backoff.prototype.next = function () { - this.time *= this.factor; + var next = this.time * this.factor; + + if (next > this.max) { + this.time = this.max; + return this.max; + } + + this.time = next; return this.time; }; @@ -33,13 +46,29 @@ Backoff.prototype.reset = function () { this.time = options.time || 50; this.factor = options.factor || 2; + this.max = options.max || 1 * 60 * 1000; return this; }; /** - * Create a websocket client and handle reconnect backoff logic. - * @param {String} url - A preformatted url (starts with ws://) + * Schedules the next connection, according to the backoff. + * @param {Peer} peer - A peer instance. + * @return {Timeout} - The timeout value from `setTimeout`. + */ +function scheduleReconnect (peer) { + var backoff = peer.backoff; + var time = backoff.time; + backoff.next(); + + var reconnect = peer.connect.bind(peer); + + return setTimeout(reconnect, time); +} + +/** + * Handles reconnections and defers messages until the socket is ready. + * @param {String} url - The address to connect to. * @param {Object} [options] - Override how the socket is managed. * @param {Object} options.backoff - Backoff options (see the constructor). * @class @@ -49,14 +78,38 @@ function Peer (url, options) { return new Peer(url, options); } + // Extend EventEmitter. + Emitter.call(this); + this.setMaxListeners(Infinity); + this.options = options || {}; - // Messages sent while offline. - this.offline = []; + // Messages sent before the socket is ready. + this.deferredMsgs = []; this.url = Peer.formatURL(url); this.backoff = new Backoff(this.options.backoff); - this.retry(url); + + // Set up the websocket. + this.connect(); + + var peer = this; + var reconnect = scheduleReconnect.bind(null, peer); + + // Handle reconnection. + this.on('close', reconnect); + this.on('error', function (error) { + if (error.code === 'ECONNREFUSED') { + reconnect(); + } + }); + + // Send deferred messages. + this.on('open', function () { + peer.drainQueue(); + peer.backoff.reset(); + }); + } /** @@ -70,93 +123,48 @@ Peer.formatURL = function (url) { return url.replace('http', 'ws'); }; +util.inherits(Peer, Emitter); var API = Peer.prototype; /** * Attempts a websocket connection. - * @param {String} url - The websocket URL. * @return {WebSocket} - The new websocket instance. */ -API.retry = function () { +API.connect = function () { var url = this.url; + // Open a new websocket. var socket = new WebSocket(url); + + // Re-use the previous listeners. + socket._events = this._events; + this.socket = socket; - this.retryOnDisconnect(socket); - - this.sendOnConnection(); - return socket; }; /** - * Sends the messages that couldn't be sent before once - * the connection is open. + * Sends all the messages in the deferred queue. * @return {Peer} - The context. */ -API.sendOnConnection = function () { +API.drainQueue = function () { var peer = this; - var queue = this.offline; - var socket = this.socket; - // Wait for the socket to connect. - socket.once('open', function () { - queue.forEach(function (msg) { - socket.send(msg); - }); - - peer.offline = []; + this.deferredMsgs.forEach(function (msg) { + peer.send(msg); }); + // Reset the queue. + this.deferredMsgs = []; + return this; }; -/** - * Schedules the next retry, according to the backoff. - * @param {Peer} peer - A peer instance. - * @return {Timeout} - The timeout value from `setTimeout`. - */ -function schedule (peer) { - var backoff = peer.backoff; - var time = backoff.time; - backoff.next(); - - return setTimeout(function () { - var socket = peer.retry(); - - // Successfully reconnected? Reset the backoff. - socket.once('open', backoff.reset.bind(backoff)); - }, time); -} - -/** - * Attaches handlers to the socket, attempting reconnection - * when it's closed. - * @param {WebSocket} socket - The websocket instance to bind to. - * @return {WebSocket} - The same websocket. - */ -API.retryOnDisconnect = function (socket) { - var peer = this; - - // Listen for socket close events. - socket.once('close', function () { - schedule(peer); - }); - - socket.on('error', function (error) { - if (error.code === 'ECONNREFUSED') { - schedule(peer); - } - }); - - return socket; -}; - /** * Send data through the socket, or add it to a queue - * of offline requests if it's not ready yet. - * @param {String} msg - The data to send. + * of deferred messages if it's not ready yet. + * @param {Mixed} msg - String, or anything that JSON can handle. * @return {Peer} - The context. */ API.send = function (msg) { @@ -164,10 +172,16 @@ API.send = function (msg) { var state = socket.readyState; var ready = socket.OPEN; + // Make sure it's a string. + if (typeof msg !== 'string') { + msg = JSON.stringify(msg); + } + + // Make sure the socket is ready. if (state === ready) { socket.send(msg); } else { - this.offline.push(msg); + this.deferredMsgs.push(msg); } return this; diff --git a/lib/wsp/Pool.js b/lib/wsp/Pool.js new file mode 100644 index 00000000..542c0482 --- /dev/null +++ b/lib/wsp/Pool.js @@ -0,0 +1,101 @@ +'use strict'; + +/** + * Simpler interface over a collection of sockets. Works with + * WebSocket clients, or sockets from a WebSocket server. + * @class + */ +function Pool () { + if (!(this instanceof Pool)) { + return new Pool(); + } + + // Maps IDs to sockets. + this.sockets = {}; +} + +var API = Pool.prototype; + +/** + * Returns the socket by the given ID. + * @param {String} id - The unique socket ID. + * @return {WebSocket|Null} - The WebSocket, if found. + */ +API.get = function (id) { + return this.sockets[id] || null; +}; + +/** + * Adds a socket to the pool. + * @param {String} id - The socket ID. + * @param {WebSocket} socket - A websocket instance. + * @return {Pool} - The context. + */ +API.add = function (id, socket) { + this.sockets[id] = socket; + + return this; +}; + +/** + * Removes a socket from the pool. + * @param {String} id - The ID of the socket to remove. + * @return {Boolean} - Whether the pool contained the socket. + */ +API.remove = function (id) { + var sockets = this.sockets; + var hasSocket = sockets.hasOwnProperty(id); + + if (hasSocket) { + delete sockets[id]; + } + + return hasSocket; +}; + +/** + * Creates a filtered pool of sockets. Works the same as Array#filter. + * @param {Function} fn - Called for each socket in the pool. + * @param {Mixed} [_this] - The `this` context to use when invoking + * the callback. + * @return {Pool} - A new, filtered socket pool. + */ +API.filter = function (fn, _this) { + var filtered = Pool(); + var pool = this; + + _this = _this || pool; + + Object.keys(this.sockets).forEach(function (id) { + var socket = pool.sockets[id]; + + var shouldAdd = fn.call(_this, socket, id, pool); + + // Add it to the new pool. + if (shouldAdd) { + filtered.add(id, socket); + } + }); + + return filtered; +}; + +/** + * Send a message through each socket in the pool. + * @param {String} msg - The message to send. + * @return {Number} - How many sockets the message was sent to. + */ +API.send = function (msg) { + var pool = this; + + var ids = Object.keys(this.sockets); + + ids.forEach(function (id) { + var socket = pool.sockets[id]; + socket.send(msg); + }); + + return ids.length; +}; + +module.exports = Pool; diff --git a/lib/wsp/client.js b/lib/wsp/client.js index d39a53df..80b408f6 100644 --- a/lib/wsp/client.js +++ b/lib/wsp/client.js @@ -1,460 +1,98 @@ -/* eslint-env node*/ /* eslint-disable - require-jsdoc, no-warning-comments, no-underscore-dangle, - max-params, */ 'use strict'; var Gun = require('../../gun'); -var WS = require('ws'); +var Socket = require('./Peer'); +var Pool = require('./Pool'); -var Tab = {}; -Tab.on = Gun.on; -Tab.peers = (function () { +// Maps URLs to sockets. +// Shared between all gun instances. +var sockets = Pool(); +var sid = Gun.text.random(); - function Peer (peers) { - if (!Peer.is(this)) { - return new Peer(peers); - } +/** + * Take a map of URLs pointing to options and ensure the + * urls are using the WS protocol. + * @param {Object} peers - Any object with URLs as keys. + * @return {Object} - Object with normalized URL keys. + */ +function normalizeURLs (peers) { + var formatted = {}; - this.peers = peers; - } + Object.keys(peers).forEach(function (url) { + var options = peers[url]; + var id = Socket.formatURL(url); + formatted[id] = options; + }); - Peer.is = function (peer) { - return peer instanceof Peer; - }; + return formatted; +} - function map (peer, url) { - var msg = this.msg; - var opt = this.opt || {}; - opt.out = true; - Peer.request(url, msg, null, opt); - } +/** + * Turns a map of URLs into a socket pool. + * @param {Object} peers - Any object with URLs as keys. + * @return {Pool} - A pool of sockets corresponding to the URLs. + */ +function getSocketSubset (peers) { + var urls = normalizeURLs(peers); - Peer.prototype.send = function (msg, opt) { - Peer.request.each(this.peers, map, { - msg: msg, - opt: opt, - }); - }; + return sockets.filter(function (socket) { + return urls.hasOwnProperty(socket.url); + }); +} - Peer.request = (function () { - - function request (base, body, cb, opt) { - - var obj = base.length ? { base: base } : {}; - obj.base = opt.base || base; - obj.body = opt.body || body; - obj.headers = opt.headers; - obj.url = opt.url; - obj.out = opt.out; - cb = cb || function () {}; - - if (!obj.base) { - return; - } - - request.transport(obj, cb); - } - - request.createServer = function (fn) { - request.createServer.list.push(fn); - }; - - request.createServer.ing = function (req, cb) { - var index = request.createServer.list.length; - var server; - while (index) { - index -= 1; - server = request.createServer.list[index] || function () {}; - server(req, cb); - } - }; - - request.createServer.list = []; - request.back = 2; - request.backoff = 2; - - request.transport = function (opt, cb) { - if (request.ws(opt, cb)) { - return; - } - }; - - request.ws = function (opt, cb, req) { - var ws; - if (!WS) { - return false; - } - - ws = request.ws.peers[opt.base]; - if (ws) { - 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']]) { - var rid = 'WS' + - new Date().getTime() + - '.' + - Math.floor((Math.random() * 65535) + 1); - - req.headers['ws-rid'] = rid; - - ws.cbs[rid] = function (err, res) { - if (!res || res.body || res.end) { - delete ws.cbs[req.headers['ws-rid']]; - } - - cb(err, res); - }; - } - - if (!ws.readyState) { - setTimeout(function () { - request.ws(opt, cb, req); - }, 100); - - return true; - } - - ws.sending = true; - ws.send(JSON.stringify(req)); - return true; - } - - if (ws === false) { - return false; - } - - var wsURL = opt.base.replace('http', 'ws'); - - ws = request.ws.peers[opt.base] = new WS(wsURL); - ws.cbs = {}; - - ws.onopen = function () { - request.back = 2; - request.ws(opt, cb); - }; - - ws.onclose = function (event) { - - if (!ws || !event) { - return; - } - - if (ws.close instanceof Function) { - ws.close(); - } - - if (!ws.sending) { - ws = request.ws.peers[opt.base] = false; - request.transport(opt, cb); - return; - } - - request.each(ws.cbs, function (cb) { - cb({ - err: 'WebSocket disconnected!', - code: ws.sending ? (ws || {}).err || event.code : -1, - }); - }); - - // This will make the next request try to reconnect - ws = request.ws.peers[opt.base] = null; - - // TODO: Have the driver handle this! - setTimeout(function () { - - // opt here is a race condition, - // is it not? Does this matter? - request.ws(opt, function () {}); - }, request.back *= request.backoff); - }; - - ws.onmessage = function (msg) { - var res; - if (!msg || !msg.data) { - return; - } - try { - res = JSON.parse(msg.data); - } catch (error) { - return; - } - if (!res) { - return; - } - res.headers = res.headers || {}; - if (res.headers['ws-rid']) { - var cb = ws.cbs[res.headers['ws-rid']] || function () {}; - cb(null, res); - return; - } - - // emit extra events. - if (res.body) { - request.createServer.ing(res, function (res) { - res.out = true; - request(opt.base, null, null, res); - }); - } - }; - - ws.onerror = function (error) { - (ws || {}).err = error; - }; - - return true; - }; - request.ws.peers = {}; - request.ws.cbs = {}; - - 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); - } - } - }; - - return request; - }()); - - return Peer; -}()); - -// Handle read requests. -Gun.on('get', function (at) { - var gun = at.gun; - var opt = at.opt || {}; +Gun.on('out', function (ctx) { + var gun = ctx.gun; + var opt = ctx.opt || {}; var 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!'); - at.gun.Back(-1).on('in', {'@': at['#']}); - + if (!peers) { return; } - // Create a new message. - var msg = { + var subset = getSocketSubset(peers); - // msg ID - '#': at['#'] || Gun.text.random(9), - - // msg BODY - '$': at.get, - }; - - // Listen for a response. - // TODO: ONE? PERF! Clear out listeners, maybe with setTimeout? - Tab.on(msg['#'], function (err, data) { - var obj = { - '@': at['#'], - err: err, - put: data, - }; - - if (data) { - at.gun.Back(-1).on('out', obj); - } else { - at.gun.Back(-1).on('in', obj); - } - }); - - // Broadcast to all other peers. - Tab.peers(peers).send(msg, { - headers: { - 'gun-sid': Tab.server.sid, - }, + subset.send({ + headers: { 'gun-sid': sid }, + body: ctx, }); }); -// Handle write requests. -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!'); - at.gun.Back(-1).on('in', {'@': at['#']}); - return; - } - if (opt.websocket === false || (at.opt && at.opt.websocket === false)) { - return; - } - var msg = { +// Open any new sockets listed, +// adding them to the global pool. +Gun.on('opt', function (context) { + var gun = context.gun; + var root = gun.Back(Infinity); - // msg ID - '#': at['#'] || Gun.text.random(9), + var peers = gun.Back('opt.peers') || {}; - // msg BODY - '$': at.put, - }; - - // TODO: ONE? PERF! Clear out listeners, maybe with setTimeout? - Tab.on(msg['#'], function (err, ok) { - at.gun.Back(-1).on('in', { - '@': at['#'], - err: err, - ok: ok, - }); - }); - - Tab.peers(peers).send(msg, { - headers: { - 'gun-sid': Tab.server.sid, - }, - }); -}); - -// REVIEW: Do I need this on a server client? -// browser/client side Server! -// TODO: BUG! Does not respect separate instances!!! -Gun.on('opt', function (at) { - if (Tab.server) { - return; - } - - var gun = at.gun; - var server = Tab.server = Tab.server || {}; - var tmp; - - server.sid = Gun.text.random(); - - Tab.peers.request.createServer(function (req, res) { - - // Validate request. - if (!req || !res || !req.body || !req.headers) { + Gun.obj.map(peers, function (options, url) { + if (sockets[url]) { return; } - var msg = req.body; + var socket = Socket(url, options); + sockets.add(url, socket); - // AUTH for non-replies. - if (server.msg(msg['#'])) { - return; - } + socket.on('message', function (msg) { + var request; - // no need to process. - if (msg['@']) { - if (Tab.ons[tmp = msg['@'] || msg['#']]) { - Tab.on(tmp, [msg['!'], msg.$]); - } - return; - } - - if (msg.$ && msg.$['#']) { - server.get(req, res); - return; - } - - server.put(req, res); - }); - - server.get = function (req, cb) { - var body = req.body; - var lex = body.$; - var graph = gun._.root._.graph; - var node; - - // Don't reply to data we don't have it in memory. - // TODO: Add localStorage? - if (!(node = graph[lex['#']])) { - return; - } - - cb({ - body: { - '#': server.msg(), - '@': body['#'], - '$': node, - }, - }); - }; - - server.put = function (req, cb) { - var body = req.body, graph = body.$; - var __ = gun._.root._; - - // filter out what we don't have in memory. - if (!(graph = Gun.obj.map(graph, function (node, soul, map) { - if (!__.path[soul]) { + try { + request = JSON.parse(msg); + } catch (error) { return; } - map(soul, node); - }))) { - return; - } - gun.on('out', { - gun: gun, - opt: { - websocket: false, - }, - put: graph, - '#': Gun.on.ask(function (ack, ev) { - if (!ack) { - return undefined; - } - ev.off(); - return cb({ - body: { - '#': server.msg(), - '@': body['#'], - '$': ack, - '!': ack.err, - }, - }); - }), + + // Validate the request. + if (!request || !request.body) { + return; + } + + root.on('in', request.body); }); - }; - - server.msg = function (id) { - if (!id) { - id = Gun.text.random(9); - server.msg.debounce[id] = Gun.time.is(); - return id; - } - - clearTimeout(server.msg.clear); - server.msg.clear = setTimeout(function () { - var now = Gun.time.is(); - Gun.obj.map(server.msg.debounce, function (time, id) { - if ((now - time) < (1000 * 60 * 5)) { - return; - } - - Gun.obj.del(server.msg.debounce, id); - }); - }, 500); - - if (server.msg.debounce[id]) { - server.msg.debounce[id] = Gun.time.is(); - return id; - } - - server.msg.debounce[id] = Gun.time.is(); - return undefined; - }; - - server.msg.debounce = server.msg.debounce || {}; + }); }); diff --git a/lib/wsp/duplicate.js b/lib/wsp/duplicate.js new file mode 100644 index 00000000..9080fee7 --- /dev/null +++ b/lib/wsp/duplicate.js @@ -0,0 +1,90 @@ +'use strict'; + +var Gun = require('../../gun'); + +var cache = {}; +var timeout = null; + +/** + * Remove all entries in the cache older than 5 minutes. + * Reschedules itself to run again when the oldest item + * might be too old. + * @return {undefined} + */ +function gc () { + var now = Date.now(); + var oldest = now; + var maxAge = 5 * 60 * 1000; + + Gun.obj.map(cache, function (time, id) { + oldest = Math.min(now, time); + + if ((now - time) < maxAge) { + return; + } + + delete cache[id]; + }); + + var done = Gun.obj.empty(cache); + + // Disengage GC. + if (done) { + timeout = null; + return; + } + + // Just how old? + var elapsed = now - oldest; + + // How long before it's too old? + var nextGC = maxAge - elapsed; + + // Schedule the next GC event. + timeout = setTimeout(gc, nextGC); +} + +/** + * Checks a memory-efficient cache to see if a string has been seen before. + * @param {String} id - A string to keep track of. + * @return {Boolean} - Whether it's been seen recently. + */ +function duplicate (id) { + + // Have we seen this ID recently? + var existing = cache.hasOwnProperty(id); + + // Add it to the cache. + duplicate.track(id); + + return existing; +} + +/** + * Starts tracking an ID as a possible future duplicate. + * @param {String} id - The ID to track. + * @return {String} - The same ID. + */ +duplicate.track = function (id) { + cache[id] = Date.now(); + + // Engage GC. + if (!timeout) { + gc(); + } + + return id; +}; + +/** + * Generate a new ID and start tracking it. + * @param {Number} [chars] - The number of characters to use. + * @return {String} - The newly created ID. + */ +duplicate.track.newID = function (chars) { + var id = Gun.text.random(chars); + + return duplicate.track(id); +}; + +module.exports = duplicate; diff --git a/lib/wsp/server-push.js b/lib/wsp/server-push.js index e4c77f7e..727f8d1e 100644 --- a/lib/wsp/server-push.js +++ b/lib/wsp/server-push.js @@ -38,54 +38,20 @@ function ready (socket, cb) { } /** - * Send a request to a list of clients. - * @param {Obejct} context - A gun request context. + * Send a message to a group of clients. + * @param {Obejct} msg - An http envelope-like message. * @param {Object} clients - IDs mapped to socket instances. - * @param {Function} cb - Called for each response. * @return {undefined} */ -function request (context, clients, cb) { +function send (msg, clients) { Gun.obj.map(clients, function (client) { ready(client, function () { - var msg = { - headers: {}, - body: { - '#': Gun.on.ask(cb), - '$': context.get, - }, - }; - var serialized = JSON.stringify(msg); client.send(serialized); }); }); } -/** - * Pushes a graph update to a collection of clients. - * @param {Object} context - The context object passed by gun. - * @param {Object} clients - An object mapping URLs to clients. - * @param {Function} cb - Invoked on each client response. - * @return {undefined} - */ -function update (context, clients, cb) { - Gun.obj.map(clients, function (client) { - ready(client, function () { - var msg = { - headers: {}, - body: { - '#': Gun.on.ask(cb), - '$': context.put, - }, - }; - - var serialized = JSON.stringify(msg); - - client.send(serialized); - }); - }); -} - /** * Attaches server push middleware to gun. * @param {Gun} gun - The gun instance to attach to. * @param {WebSocket.Server} server - A websocket server instance. @@ -96,60 +62,35 @@ function attach (gun, server) { root._.servers = root._.servers || []; root._.servers.push(server); var pool = {}; - + var sid = Gun.text.random(); server.on('connection', function (socket) { socket.id = socket.id || Gun.text.random(10); pool[socket.id] = socket; - + /* socket.on('message', function (message) { var data = Gun.obj.ify(message); if (!data || !data.body) { return; } - - var msg = data.body; - - if (msg['@']) { - Gun.on.ack(msg['@'], [msg['!'], msg.$]); - return; - } + root.on('in', data.body); }); - + */ socket.once('close', function () { delete pool[socket.id]; }); }); - Gun.on('get', function (context) { - if (!isUsingServer(context.gun, server)) { - return; - } - request(context, pool, function (err, data) { - var response = { - '@': context['#'], - put: data, - err: err, - }; - - var root = context.gun.Back(Infinity); - - root.on(data ? 'out' : 'in', response); - }); - }); - - Gun.on('put', function (context) { - if (!isUsingServer(context.gun, server)) { + Gun.on('out', function (context) { + if (!isUsingServer(context.gun, server) || Gun.obj.empty(pool)) { return; } - update(context, pool, function (err, data) { - var ack = { - '!': err || null, - '$': data.$, - }; - Gun.on.ack(context, ack); - }); + var msg = { + headers: { 'gun-sid': sid }, + body: context, + }; + send(msg, pool); }); } diff --git a/lib/wsp/server.js b/lib/wsp/server.js index b4e6fd16..3c56a425 100644 --- a/lib/wsp/server.js +++ b/lib/wsp/server.js @@ -1,22 +1,24 @@ -var Gun = require('../../gun') -, formidable = require('formidable') -, http = require('../http') -, url = require('url') -, wsp = {} -, WS = require('ws') -, WSS = WS.Server -, attach = require('./server-push'); +/* eslint-disable require-jsdoc, no-underscore-dangle */ +'use strict'; +var Gun = require('../../gun'); +var http = require('../http'); +var url = require('url'); +var WS = require('ws'); +var WSS = WS.Server; +var attach = require('./server-push'); // Handles server to server sync. require('./client.js'); -Gun.on('opt', function(at){ +Gun.on('opt', function (at) { var gun = at.gun, opt = at.opt; gun.__ = at.root._; gun.__.opt.ws = opt.ws = gun.__.opt.ws || opt.ws || {}; - function start(server, port, app){ - if(app && app.use){ app.use(gun.wsp.server) } + function start (server, port, app) { + if (app && app.use) { + app.use(gun.wsp.server); + } server = gun.__.opt.ws.server = gun.__.opt.ws.server || opt.ws.server || server; if (!gun.wsp.ws) { @@ -25,167 +27,218 @@ Gun.on('opt', function(at){ } gun.wsp.ws = gun.wsp.ws || new WSS(gun.__.opt.ws); - require('./ws')(gun.wsp.ws, function(req, res){ + require('./ws')(gun.wsp.ws, function (req, res) { var ws = this; - req.headers['gun-sid'] = ws.sid = ws.sid? ws.sid : req.headers['gun-sid']; - ws.sub = ws.sub || gun.wsp.on('network', function(msg, ev){ - if(!ws || !ws.send || !ws._socket || !ws._socket.writable){ return ev.off() } - if(!msg || (msg.headers && msg.headers['gun-sid'] === ws.sid)){ return } - if(msg && msg.headers){ delete msg.headers['ws-rid'] } + req.headers['gun-sid'] = ws.sid = ws.sid ? ws.sid : req.headers['gun-sid']; + ws.sub = ws.sub || gun.wsp.on('network', function (msg, ev) { + if (!ws || !ws.send || !ws._socket || !ws._socket.writable) { return ev.off(); } + if (!msg || (msg.headers && msg.headers['gun-sid'] === ws.sid)) { return; } + if (msg && msg.headers) { delete msg.headers['ws-rid']; } // TODO: BUG? ^ What if other peers want to ack? Do they use the ws-rid or a gun declared id? - try{ws.send(Gun.text.ify(msg)); - }catch(e){} // juuuust in case. + try { ws.send(Gun.text.ify(msg)); + } catch (e) {} // juuuust in case. }); gun.wsp.wire(req, res); }, {headers: {'ws-rid': 1, 'gun-sid': 1}}); gun.__.opt.ws.port = gun.__.opt.ws.port || opt.ws.port || port || 80; } - var wsp = gun.wsp = gun.wsp || function(server){ - if(!server){ return gun } - if(Gun.fns.is(server.address)){ - if(server.address()){ + var wsp = gun.wsp = gun.wsp || function (server) { + if (!server) { return gun; } + if (Gun.fns.is(server.address)) { + if (server.address()) { start(server, server.address().port); return gun; } } - if(Gun.fns.is(server.get) && server.get('port')){ + if (Gun.fns.is(server.get) && server.get('port')) { start(server, server.get('port')); return gun; } var listen = server.listen; - server.listen = function(port){ + server.listen = function (port) { var serve = listen.apply(server, arguments); start(serve, port, server); return serve; - } + }; return gun; - } + }; gun.wsp.on = gun.wsp.on || Gun.on; gun.wsp.regex = gun.wsp.regex || opt.route || opt.path || /^\/gun/i; gun.wsp.poll = gun.wsp.poll || opt.poll || 1; gun.wsp.pull = gun.wsp.pull || opt.pull || gun.wsp.poll * 1000; - gun.wsp.server = gun.wsp.server || function(req, res, next){ // http - next = next || function(){}; - if(!req || !res){ return next(), false } - if(!req.url){ return next(), false } - if(!req.method){ return next(), false } + gun.wsp.server = gun.wsp.server || function (req, res, next) { // http + next = next || function () {}; + if (!req || !res) { return next(), false; } + if (!req.url) { return next(), false; } + if (!req.method) { return next(), false; } var msg = {}; msg.url = url.parse(req.url, true); - if(!gun.wsp.regex.test(msg.url.pathname)){ return next(), false } // TODO: BUG! If the option isn't a regex then this will fail! - if(msg.url.pathname.replace(gun.wsp.regex,'').slice(0,3).toLowerCase() === '.js'){ + if (!gun.wsp.regex.test(msg.url.pathname)) { return next(), false; } // TODO: BUG! If the option isn't a regex then this will fail! + if (msg.url.pathname.replace(gun.wsp.regex, '').slice(0, 3).toLowerCase() === '.js') { res.writeHead(200, {'Content-Type': 'text/javascript'}); res.end(gun.wsp.js = gun.wsp.js || require('fs').readFileSync(__dirname + '/../../gun.js')); // gun server is caching the gun library for the client return true; } - if(!req.upgrade){ + if (!req.upgrade) { next(); return false; } - return http(req, res, function(req, res){ - if(!req){ return next() } + return http(req, res, function (req, res) { + if (!req) { return next(); } var stream, cb = res = require('../jsonp')(req, res); - if(req.headers && (stream = req.headers['gun-sid'])){ + if (req.headers && (stream = req.headers['gun-sid'])) { stream = (gun.wsp.peers = gun.wsp.peers || {})[stream] = gun.wsp.peers[stream] || {sid: stream}; - stream.drain = stream.drain || function(res){ - if(!res || !stream || !stream.queue || !stream.queue.length){ return } + stream.drain = stream.drain || function (res) { + if (!res || !stream || !stream.queue || !stream.queue.length) { return; } res({headers: {'gun-sid': stream.sid}, body: stream.queue }); - stream.off = setTimeout(function(){ stream = null }, gun.wsp.pull); + stream.off = setTimeout(function () { stream = null; }, gun.wsp.pull); stream.reply = stream.queue = null; return true; - } - stream.sub = stream.sub || gun.wsp.on('network', function(req, ev){ - if(!stream){ return ev.off() } // self cleans up after itself! - if(!req || (req.headers && req.headers['gun-sid'] === stream.sid)){ return } + }; + stream.sub = stream.sub || gun.wsp.on('network', function (req, ev) { + if (!stream) { return ev.off(); } // self cleans up after itself! + if (!req || (req.headers && req.headers['gun-sid'] === stream.sid)) { return; } (stream.queue = stream.queue || []).push(req); stream.drain(stream.reply); }); - cb = function(r){ (r.headers||{}).poll = gun.wsp.poll; res(r) } + cb = function (r) { (r.headers || {}).poll = gun.wsp.poll; res(r); }; clearTimeout(stream.off); - if(req.headers.pull){ - if(stream.drain(cb)){ return } + if (req.headers.pull) { + if (stream.drain(cb)) { return; } return stream.reply = cb; } } gun.wsp.wire(req, cb); }), true; - } - if((gun.__.opt.maxSockets = opt.maxSockets || gun.__.opt.maxSockets) !== false){ + }; + 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){ + 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(){ + 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.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]){ + }, 500); + if (id = gun.wsp.msg.debounce[id]) { return gun.wsp.msg.debounce[id] = Gun.time.is(), id; } gun.wsp.msg.debounce[id] = Gun.time.is(); return; }; 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, res){ - if(!req || !res || !req.body || !req.headers){ return } - if(req.url){ req.url = url.format(req.url) } - var msg = req.body; - // AUTH for non-replies. - if(gun.wsp.msg(msg['#'])){ return } - gun.wsp.on('network', Gun.obj.copy(req)); - if(msg['@']){ return } // no need to process. - if(msg['$'] && msg['$']['#']){ return tran.get(req, res) } - //if(Gun.is.lex(msg['$'])){ return tran.get(req, res) } - else { return tran.put(req, res) } - cb({body: {hello: 'world'}}); - // TODO: BUG! server put should push. + 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 sent to. + // all states, implemented with GET, are replied to the + // source that asked for it. + function tran (req, res) { + if (!req || !res || !req.body || !req.headers) { + return; + } + if (req.url) { + req.url = url.format(req.url); + } + // var msg = req.body; + gun.on('in', req.body); + // // AUTH for non-replies. + // if(gun.wsp.msg(msg['#'])){ return } + // gun.wsp.on('network', Gun.obj.copy(req)); + // if(msg['@']){ return } // no need to process. + // if(msg['$'] && msg['$']['#']){ return tran.get(req, res) } + // //if(Gun.is.lex(msg['$'])){ return tran.get(req, res) } + // else { return tran.put(req, res) } + // cb({body: {hello: 'world'}}); + // // TODO: BUG! server put should push. } - tran.get = function(req, cb){ - var body = req.body, lex = body['$'], reply = {headers: {'Content-Type': tran.json}}, opt; - gun.on('out', {gun: gun, get: lex, req: 1, '#': Gun.on.ask(function(at, ev){ - ev.off(); - var graph = at.put; - return cb({headers: reply.headers, body: { - '#': gun.wsp.msg(), - '@': body['#'], - '$': graph, - '!': at.err - }}); - })}); - } - 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 body = req.body, graph = body['$'], reply = {headers: {'Content-Type': tran.json}}, opt; - gun.on('out', {gun: gun, put: graph, '#': Gun.on.ask(function(ack, ev){ - //Gun.on('put', {gun: gun, put: graph, '#': Gun.on.ask(function(ack, ev){ - ev.off(); - return cb({headers: reply.headers, body: { - '#': gun.wsp.msg(), - '@': body['#'], - '$': ack, - '!': ack.err - }}); - })}); - } - gun.wsp.on('network', function(rq){ - // TODO: MARK! You should move the networking events to here, not in WSS only. - }); + tran.get = function (req, cb) { + var body = req.body; + var lex = body.$; + var reply = { + headers: { 'Content-Type': tran.json }, + }; + + var graph = gun.Back(Infinity)._.graph; + var node = graph[lex['#']]; + var result = Gun.graph.ify(node); + + if (node) { + cb({ + headers: reply.headers, + body: { + '#': gun.wsp.msg(), + '@': body['#'], + '$': result, + }, + }); + + return; + } + + gun.on('out', { + gun: gun, + get: lex, + req: 1, + '#': body['#'] || Gun.on.ask(function (at, ev) { + ev.off(); + var graph = at.put; + return cb({ + headers: reply.headers, + body: { + '#': gun.wsp.msg(), + '@': body['#'], + '$': graph, + '!': at.err, + }, + }); + }), + }); + }; + + 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 body = req.body; + var graph = body.$; + var reply = { + headers: { 'Content-Type': tran.json }, + }; + + gun.on('out', { + gun: gun, + put: graph, + '#': Gun.on.ask(function (ack, ev) { + ev.off(); + return cb({ + headers: reply.headers, + body: { + '#': gun.wsp.msg(), + '@': body['#'], + '$': ack, + '!': ack.err, + }, + }); + }), + }); + }; + tran.json = 'application/json'; return tran; }()); - if(opt.server){ + + if (opt.server) { wsp(opt.server); } }); diff --git a/lib/wsp/ws.js b/lib/wsp/ws.js index 45dc100a..5b95932d 100644 --- a/lib/wsp/ws.js +++ b/lib/wsp/ws.js @@ -1,4 +1,4 @@ -var Gun = require('../../gun') +var Gun = require('../../gun') , url = require('url'); module.exports = function(wss, server, opt){ wss.on('connection', function(ws){ @@ -27,7 +27,7 @@ module.exports = function(wss, server, opt){ (reply.headers = reply.headers || {})['ws-rid'] = msg.headers['ws-rid']; } try{ws.send(Gun.text.ify(reply)); - }catch(e){} // juuuust in case. + }catch(e){} // juuuust in case. }); }); ws.off = function(m){ diff --git a/package.json b/package.json index 18e8b3f1..ab9f8c81 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,9 @@ "main": "index.js", "scripts": { "start": "node examples/http.js 8080", + "prepublish": "npm run unbuild && uglifyjs gun.js -o gun.min.js -c -m", "test": "mocha", + "docker": "hooks/build", "unbuild": "node lib/unbuild.js" }, "repository": { @@ -49,9 +51,10 @@ "ws": "~>1.0.1" }, "devDependencies": { - "mocha": "~>1.9.0", "express": "~>4.13.4", + "mocha": "~>1.9.0", "panic-server": "~>0.3.0", - "selenium-webdriver": "~>2.53.2" + "selenium-webdriver": "~>2.53.2", + "uglify-js": "^2.2.0" } } diff --git a/src/.gitignore b/src/.gitignore new file mode 100644 index 00000000..5e7d2734 --- /dev/null +++ b/src/.gitignore @@ -0,0 +1,4 @@ +# Ignore everything in this directory +* +# Except this file +!.gitignore