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 [](https://npmjs.org/package/gun) [](https://travis-ci.org/amark/gun) [](https://gitter.im/amark/gun?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
+
+[](https://npmjs.org/package/gun) [](https://travis-ci.org/amark/gun) [](https://gitter.im/amark/gun?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
+[](https://hub.docker.com/r/gundb/gun/) [](https://microbadger.com/images/gundb/gun "Get your own image badge on microbadger.com") [](https://hub.docker.com/r/gundb/gun/) [](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://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 [](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