mirror of
https://github.com/amark/gun.git
synced 2025-03-30 15:08:33 +00:00
1180 lines
45 KiB
JavaScript
1180 lines
45 KiB
JavaScript
;(function(){
|
|
function Gun(opt){
|
|
var gun = this;
|
|
if(!Gun.is(gun)){
|
|
return new Gun(opt);
|
|
}
|
|
gun.opt(opt);
|
|
}
|
|
Gun._ = {
|
|
soul: '#'
|
|
,meta: '_'
|
|
,HAM: '>'
|
|
}
|
|
;(function(Gun){
|
|
Gun.version = 0.9;
|
|
Gun.is = function(gun){ return (gun instanceof Gun)? true : false }
|
|
Gun.is.value = function(v){ // null, binary, number (!Infinity), text, or a rel.
|
|
if(v === null){ return true } // deletes
|
|
if(v === Infinity){ return false } // we want this to be, but JSON does not support it, sad face.
|
|
if(Gun.bi.is(v)
|
|
|| Gun.num.is(v)
|
|
|| Gun.text.is(v)){
|
|
return true; // simple values
|
|
}
|
|
var id;
|
|
if(id = Gun.is.soul(v)){
|
|
return id;
|
|
}
|
|
return false;
|
|
}
|
|
Gun.is.value.as = function(v){
|
|
return Gun.is.value(v)? v : null;
|
|
}
|
|
Gun.is.soul = function(v){
|
|
if(Gun.obj.is(v)){
|
|
var id;
|
|
Gun.obj.map(v, function(soul, field){
|
|
if(id){ return id = false } // if ID is already defined AND we're still looping through the object, it is invalid.
|
|
if(field == Gun._.soul && Gun.text.is(soul)){
|
|
id = soul; // we found the soul!
|
|
} else {
|
|
return id = false; // if there exists anything else on the object, that isn't the soul, then it is invalid.
|
|
}
|
|
});
|
|
if(id){
|
|
return id;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
Gun.is.soul.on = function(n){ return (n && n._ && n._[Gun._.soul]) || false }
|
|
Gun.is.node = function(node, cb){
|
|
if(!Gun.obj.is(node)){ return false }
|
|
if(Gun.is.soul.on(node)){
|
|
return !Gun.obj.map(node, function(value, field){ // need to invert this, because the way we check for this is via a negation.
|
|
if(field == Gun._.meta){ return } // skip this.
|
|
if(!Gun.is.value(value)){ return true } // it is true that this is an invalid node.
|
|
if(cb){ cb(value, field) }
|
|
});
|
|
}
|
|
return false;
|
|
}
|
|
Gun.is.graph = function(graph, cb, fn){
|
|
var exist = false;
|
|
if(!Gun.obj.is(graph)){ return false }
|
|
return !Gun.obj.map(graph, function(node, soul){ // need to invert this, because the way we check for this is via a negation.
|
|
if(!node || soul !== Gun.is.soul.on(node) || !Gun.is.node(node, fn)){ return true } // it is true that this is an invalid graph.
|
|
if(cb){ cb(node, soul) }
|
|
exist = true;
|
|
}) && exist;
|
|
}
|
|
// Gun.ify // the serializer is too long for right here, it has been relocated towards the bottom.
|
|
Gun.union = function(graph, prime){
|
|
var context = Gun.shot();
|
|
context.nodes = {};
|
|
context('done');
|
|
context('change');
|
|
Gun.obj.map(prime, function(node, soul){
|
|
var vertex = graph[soul], env;
|
|
if(!vertex){ // disjoint
|
|
context.nodes[node._[Gun._.soul]] = graph[node._[Gun._.soul]] = node;
|
|
context('change').fire(node);
|
|
return;
|
|
}
|
|
env = Gun.HAM(vertex, node, function(current, field, deltaValue){
|
|
if(!current){ return }
|
|
var change = {};
|
|
current[field] = change[field] = deltaValue; // current and vertex are the same
|
|
current._[Gun._.HAM][field] = node._[Gun._.HAM][field];
|
|
change._ = current._;
|
|
context.nodes[change._[Gun._.soul]] = change;
|
|
context('change').fire(change);
|
|
}).upper(function(c){
|
|
context.err = c.err;
|
|
context.up -= 1;
|
|
if(!context.up){
|
|
context('done').fire(context.err, context);
|
|
}
|
|
});
|
|
context.up += env.up;
|
|
});
|
|
if(!context.up){
|
|
context('done').fire(context.err, context);
|
|
}
|
|
return context;
|
|
}
|
|
Gun.HAM = function(current, delta, each){ // HAM only handles primitives values, all other data structures need to be built ontop and reduce to HAM.
|
|
function HAM(machineState, incomingState, currentState, incomingValue, currentValue){ // TODO: Lester's comments on roll backs could be vulnerable to divergence, investigate!
|
|
if(machineState < incomingState){
|
|
// the incoming value is outside the boundary of the machine's state, it must be reprocessed in another state.
|
|
return {amnesiaQuarantine: true};
|
|
}
|
|
if(incomingState < currentState){
|
|
// the incoming value is within the boundary of the machine's state, but not within the range.
|
|
return {quarantineState: true};
|
|
}
|
|
if(currentState < incomingState){
|
|
// the incoming value is within both the boundary and the range of the machine's state.
|
|
return {converge: true, incoming: true};
|
|
}
|
|
if(incomingState === currentState){
|
|
if(incomingValue === currentValue){ // Note: while these are practically the same, the deltas could be technically different
|
|
return {state: true};
|
|
}
|
|
/*
|
|
The following is a naive implementation, but will always work.
|
|
Never change it unless you have specific needs that absolutely require it.
|
|
If changed, your data will diverge unless you guarantee every peer's algorithm has also been changed to be the same.
|
|
As a result, it is highly discouraged to modify despite the fact that it is naive,
|
|
because convergence (data integrity) is generally more important.
|
|
Any difference in this algorithm must be given a new and different name.
|
|
*/
|
|
if(String(incomingValue) < String(currentValue)){ // String only works on primitive values!
|
|
return {converge: true, current: true};
|
|
}
|
|
if(String(currentValue) < String(incomingValue)){ // String only works on primitive values!
|
|
return {converge: true, incoming: true};
|
|
}
|
|
}
|
|
return {err: "you have not properly handled recursion through your data or filtered it as JSON"};
|
|
}
|
|
var context = Gun.shot();
|
|
context.HAM = {};
|
|
context.states = {};
|
|
context.states.delta = delta._[Gun._.HAM];
|
|
context.states.current = current._[Gun._.HAM] = current._[Gun._.HAM] || {};
|
|
context('lower');context('upper');context.up = context.up || 0;
|
|
Gun.obj.map(delta, function update(deltaValue, field){
|
|
if(field === Gun._.meta){ return }
|
|
if(!Gun.obj.has(current, field)){ // does not need to be applied through HAM
|
|
each.call({incoming: true, converge: true}, current, field, deltaValue); // done synchronously
|
|
return;
|
|
}
|
|
var serverState = Gun.time.is();
|
|
var incomingValue = Gun.is.soul(deltaValue) || deltaValue;
|
|
var currentValue = Gun.is.soul(current[field]) || current[field];
|
|
// add more checks?
|
|
var state = HAM(serverState, context.states.delta[field], context.states.current[field], incomingValue, currentValue);
|
|
//console.log("the server state is",serverState,"with delta:current",context.states.delta[field],context.states.current[field]);
|
|
//console.log("having incoming value of",deltaValue,'and',current[field]);
|
|
if(state.err){
|
|
root.console.log(".!HYPOTHETICAL AMNESIA MACHINE ERR!.", state.err); // this error should never happen.
|
|
return;
|
|
}
|
|
if(state.state || state.quarantineState || state.current){
|
|
context('lower').fire(context, state, current, field, deltaValue);
|
|
return;
|
|
}
|
|
if(state.incoming){
|
|
each.call(state, current, field, deltaValue); // done synchronously
|
|
return;
|
|
}
|
|
if(state.amnesiaQuarantine){
|
|
context.up += 1;
|
|
Gun.schedule(context.states.delta[field], function(){
|
|
update(deltaValue, field);
|
|
context.up -= 1;
|
|
context('upper').fire(context, state, current, field, deltaValue);
|
|
});
|
|
}
|
|
});
|
|
if(!context.up){
|
|
context('upper').fire(context, {});
|
|
}
|
|
return context;
|
|
}
|
|
Gun.roulette = function(l, c){
|
|
var gun = Gun.is(this)? this : {};
|
|
if(gun._ && gun.__.opt && gun.__.opt.uuid){
|
|
if(Gun.fns.is(gun.__.opt.uuid)){
|
|
return gun.__.opt.uuid(l, c);
|
|
}
|
|
l = l || gun.__.opt.uuid.length;
|
|
}
|
|
return Gun.text.random(l, c);
|
|
}
|
|
}(Gun));
|
|
;(function(Chain){
|
|
Chain.opt = function(opt, stun){ // idempotently update or set options
|
|
var gun = this;
|
|
gun._ = gun._ || {};
|
|
gun.__ = gun.__ || {};
|
|
gun.shot = Gun.shot('then', 'err');
|
|
gun.shot.next = Gun.next();
|
|
if(opt === null){ return gun }
|
|
opt = opt || {};
|
|
gun.__.opt = gun.__.opt || {};
|
|
gun.__.keys = gun.__.keys || {};
|
|
gun.__.graph = gun.__.graph || {};
|
|
gun.__.on = gun.__.on || Gun.on.create();
|
|
if(Gun.text.is(opt)){ opt = {peers: opt} }
|
|
if(Gun.list.is(opt)){ opt = {peers: opt} }
|
|
if(Gun.text.is(opt.peers)){ opt.peers = [opt.peers] }
|
|
if(Gun.list.is(opt.peers)){ opt.peers = Gun.obj.map(opt.peers, function(n,f,m){ m(n,{}) }) }
|
|
gun.__.opt.peers = opt.peers || gun.__.opt.peers || {};
|
|
gun.__.opt.uuid = opt.uuid || gun.__.opt.uuid || {};
|
|
gun.__.opt.cb = gun.__.opt.cb || function(){};
|
|
gun.__.opt.hooks = gun.__.opt.hooks || {};
|
|
gun.__.hook = Gun.shot('then','end');
|
|
Gun.obj.map(opt.hooks, function(h, f){
|
|
if(!Gun.fns.is(h)){ return }
|
|
gun.__.opt.hooks[f] = h;
|
|
});
|
|
if(!stun){ Gun.on('opt').emit(gun, opt) }
|
|
return gun;
|
|
}
|
|
Chain.chain = function(from){
|
|
var gun = Gun(null);
|
|
from = from || this;
|
|
gun.back = from;
|
|
gun.__ = from.__;
|
|
gun._ = {};
|
|
//Gun.obj.map(from._, function(val, field){ gun._[field] = val });
|
|
return gun;
|
|
}
|
|
Chain.load = function(key, cb, opt){
|
|
var gun = this.chain();
|
|
gun.shot.next(function(next){
|
|
opt = opt || {};
|
|
cb = cb || function(){};
|
|
cb.soul = Gun.is.soul(key); // is this a key or a soul?
|
|
if(cb.soul){ // if a soul...
|
|
cb.node = gun.__.graph[cb.soul]; // then attempt to grab it directly from cache in the graph
|
|
} else { // if not...
|
|
cb.node = gun.__.keys[key]; // attempt to grab it directly from our key cache
|
|
(gun._.keys = gun._.keys || {})[key] = cb.node? 1 : 0; // set a key marker on it
|
|
}
|
|
if(!opt.force && cb.node){ // if it was in cache, then...
|
|
console.log("load via gun");
|
|
gun._.node = cb.node; // assign it to this context
|
|
return cb.call(gun, null, Gun.obj.copy(gun._.node)), next(); // frozen copy
|
|
}
|
|
// missing: hear shots! I now hook this up in other places, but we could get async/latency issues?
|
|
// We need to subscribe early? Or the transport layer handle this for us?
|
|
if(Gun.fns.is(gun.__.opt.hooks.load)){
|
|
gun.__.opt.hooks.load(key, function(err, data){
|
|
if(err){ return cb.call(gun, err) }
|
|
if(!data){ return cb.call(gun, null, data), next() }
|
|
var context = gun.union(data); // safely transform the data into the current context
|
|
if(context.err){ return cb.call(gun, context.err) } // but validate it in case of errors
|
|
gun._.node = gun.__.graph[Gun.is.soul.on(data)]; // immediately use the state in cache.
|
|
if(!cb.soul){ // and if we had loaded with a key rather than a soul
|
|
gun._.keys[key] = 1; // then set a marker that this key matches
|
|
gun.__.keys[key] = gun._.node; // and cache a pointer to the node
|
|
}
|
|
return cb.call(gun, null, Gun.obj.copy(gun._.node)), next(); // frozen copy
|
|
}, opt);
|
|
} else {
|
|
root.console.log("Warning! You have no persistence layer to load from!");
|
|
return cb.call(gun), next();
|
|
}
|
|
});
|
|
return gun;
|
|
}
|
|
Chain.key = function(key, cb){
|
|
var gun = this;
|
|
if(!gun.back){ // TODO: BUG? Does this maybe introduce bugs other than the test that it fixes?
|
|
gun = gun.chain(); // create a new context
|
|
}
|
|
gun.shot.next(function(next){
|
|
cb = cb || function(){};
|
|
if(Gun.obj.is(key)){ // if key is an object then we get the soul directly from it
|
|
Gun.obj.map(key, function(soul, field){ return cb.key = field, cb.soul = soul });
|
|
} else { // or else
|
|
cb.key = key; // the key is the key
|
|
}
|
|
if(gun._.node){ // if it is in cache
|
|
cb.soul = Gun.is.soul.on(gun._.node);
|
|
(gun._.keys = gun._.keys || {})[cb.key] = 1; // clear the marker in this context
|
|
(gun.__.keys = gun.__.keys || {})[cb.key] = gun._.node; // and create the pointer
|
|
} else { // if it is not in cache
|
|
(gun._.keys = gun._.keys || {})[cb.key] = 0; // then set a marker on this context
|
|
}
|
|
if(Gun.fns.is(gun.__.opt.hooks.key)){
|
|
gun.__.opt.hooks.key(cb.key, cb.soul, function(err, data){ // call the hook
|
|
return cb.call(gun, err, data); // and notify how it went.
|
|
});
|
|
} else {
|
|
root.console.log("Warning! You have no key hook!");
|
|
cb.call(gun);
|
|
}
|
|
next(); // continue regardless
|
|
});
|
|
return gun;
|
|
}
|
|
/*
|
|
how many different ways can we get something? ONLY THE FIRST ONE IS SUPPORTED, the others might become plugins.
|
|
Find via a singular path
|
|
.path('blah').get(blah);
|
|
Find via multiple paths with the callback getting called many times
|
|
.path('foo', 'bar').get(foorOrBar);
|
|
Find via multiple paths with the callback getting called once with matching arguments
|
|
.path('foo', 'bar').get(foo, bar)
|
|
Find via multiple paths with the result aggregated into an object of pre-given fields
|
|
.path('foo', 'bar').get({foo: foo, bar: bar}) || .path({a: 'foo', b: 'bar'}).get({a: foo, b: bar})
|
|
Find via multiple paths where the fields and values must match
|
|
.path({foo: val, bar: val}).get({})
|
|
Path ultimately should call .get each time, individually, for what it finds.
|
|
Things that wait and merge many things together should be an abstraction ontop of path.
|
|
*/
|
|
Chain.path = function(path, cb){ // Follow the path into the field.
|
|
var gun = this.chain(); // create a new context, changing the focal point.
|
|
cb = cb || function(){};
|
|
path = (path || '').split('.');
|
|
gun.shot.next(cb.done = function(next){ // let the previous promise resolve.
|
|
if(next){ cb.next = next }
|
|
if(!cb.next || !cb.back){ return }
|
|
cb = cb || function(){}; // fail safe our function.
|
|
(function trace(){ // create a recursive function, and immediately call it.
|
|
gun._.field = Gun.text.ify(path.shift()); // where are we at? Figure it out.
|
|
if(gun._.node && path.length && Gun.is.soul(gun._.node[gun._.field])){ // if we need to recurse more
|
|
return gun.load(val, function(err){ // and the recursion happens to be on a relation, then load it.
|
|
if(err){ return cb.call(gun, err) }
|
|
trace(gun._ = this._); // follow the context down the chain.
|
|
});
|
|
}
|
|
cb.call(gun, null, Gun.obj.copy(gun._.node), gun._.field); // frozen copy
|
|
cb.next(); // and be done, fire our gun with the context.
|
|
}(gun._.node = gun.back._.node)); // immediately call trace, setting the new context with the previous node.
|
|
});
|
|
gun.back.shot.next(function(next){
|
|
cb.back = true;
|
|
cb.done();
|
|
next();
|
|
});
|
|
return gun;
|
|
}
|
|
Chain.get = function(cb){
|
|
var gun = this; // keep using the existing context.
|
|
gun.shot.next(function(next){ // let the previous promise resolve.
|
|
next(); // continue with the chain.
|
|
cb = cb || function(){}; // fail safe our function.
|
|
if(!gun._.node){ return } // if no node, then abandon and let blank handle it.
|
|
var field = gun._.field, val = gun._.node[field]; // else attempt to get the value at the field, if we have a field.
|
|
if(field && Gun.is.soul(val)){ // if we have a field, then check to see if it is a relation
|
|
return gun.load(val, function(err, val){ // and load it.
|
|
if(err || !this._.node){ return } // handle error?
|
|
cb.call(this, val, field); // already frozen copy
|
|
});
|
|
}
|
|
cb.call(gun, gun._.field? Gun.is.value.as(val) : Gun.obj.copy(gun._.node), gun._.field); // frozen copy
|
|
});
|
|
return gun;
|
|
}
|
|
Chain.on = function(cb){ // get and then subscribe to subsequent changes.
|
|
var gun = this; // keep using the existing context.
|
|
gun.get(function(val, field){
|
|
cb = cb || function(){}; // fail safe our function.
|
|
cb.call(gun, val, field);
|
|
gun.__.on(Gun.is.soul.on(gun._.node)).event(function(delta){ // then subscribe to subsequent changes.
|
|
if(!delta || !gun._.node){ return }
|
|
if(!gun._.field){ // if we were listening to changes on the node as a whole
|
|
return cb.call(gun, Gun.obj.copy(gun._.node)); // frozen copy
|
|
}
|
|
if(Gun.obj.has(delta, gun._.field)){ // else changes on an individual property
|
|
delta = delta[gun._.field]; // grab it and
|
|
cb.call(gun, Gun.obj.is(delta)? Gun.obj.copy(delta) : Gun.is.value.as(delta), gun._.field); // frozen copy
|
|
// TODO! BUG: If delta is an object, that would suggest it is a relation which needs to get loaded.
|
|
}
|
|
});
|
|
});
|
|
return gun;
|
|
}
|
|
/*
|
|
ACID compliant? Unfortunately the vocabulary is vague, as such the following is an explicit definition:
|
|
A - Atomic, if you set a full node, or nodes of nodes, if any value is in error then nothing will be set.
|
|
If you want sets to be independent of each other, you need to set each piece of the data individually.
|
|
C - Consistency, if you use any reserved symbols or similar, the operation will be rejected as it could lead to an invalid read and thus an invalid state.
|
|
I - Isolation, the conflict resolution algorithm guarantees idempotent transactions, across every peer, regardless of any partition,
|
|
including a peer acting by itself or one having been disconnected from the network.
|
|
D - Durability, if the acknowledgement receipt is received, then the state at which the final persistence hook was called on is guaranteed to have been written.
|
|
The live state at point of confirmation may or may not be different than when it was called.
|
|
If this causes any application-level concern, it can compare against the live data by immediately reading it, or accessing the logs if enabled.
|
|
*/
|
|
Chain.set = function(val, cb, opt){ // TODO: need to turn deserializer into a trampolining function so stackoverflow doesn't happen.
|
|
var gun = this;
|
|
if(!gun.back){
|
|
gun = gun.chain(); // create a new context
|
|
}
|
|
gun.shot.next(function(next){
|
|
opt = opt || {};
|
|
cb = cb || function(){};
|
|
if(!gun._.node){
|
|
if(Gun.is.value(val) || !Gun.obj.is(val)){
|
|
return cb.call(gun, {err: Gun.log("No field exists to set the " + (typeof val) + " on.")});
|
|
}
|
|
} else
|
|
if(gun._.field){ // if a field exists, it should always be a string
|
|
var partial = {}; // in case we are doing a set on a field, not on a node
|
|
partial[gun._.field] = val; // we create a blank node with the field/value to be set
|
|
val = partial;
|
|
}
|
|
cb.states = Gun.time.is();
|
|
Gun.ify(val, function(raw, context, sub, soul){
|
|
if(val === raw){ return soul(Gun.is.soul.on(gun._.node)) }
|
|
if(gun._.node && sub && sub.path){
|
|
return gun.path(sub.path, function(err, node, field){
|
|
if(err){ cb.err = err + " (while doing a set)" } // let .done handle calling this, it may be slower but is more consistent.
|
|
if(this._.node && (field = Gun.is.soul(this._.node[this._.field]))){
|
|
return soul(field);
|
|
} soul(); // else call it anyways
|
|
});
|
|
} soul(); // else call it anyways
|
|
}).done(function(err, set){
|
|
// TODO: should be able to handle val being a relation or a gun context or a gun promise.
|
|
// TODO: BUG: IF we are setting an object, doing a partial merge, and they are reusing a frozen copy, we need to do a DIFF to update the HAM! Or else we'll get "old" HAM.
|
|
cb.root = set.root;
|
|
set.err = set.err || cb.err;
|
|
if(set.err || !cb.root){ return cb.call(gun, set.err || {err: Gun.log("No root object!")}) }
|
|
set = Gun.ify.state(set.nodes, cb.states); // set time state on nodes?
|
|
if(set.err){ return cb.call(gun, set.err) }
|
|
gun.union(set.nodes); // while this maybe should return a list of the nodes that were changed, we want to send the actual delta
|
|
gun._.node = gun.__.graph[cb.root._[Gun._.soul]] || cb.root;
|
|
if(!gun._.field){
|
|
Gun.obj.map(gun._.keys, function(yes, key){
|
|
if(yes){ return }
|
|
gun.key(key); // TODO: Feature? what about these callbacks?
|
|
});
|
|
}
|
|
if(Gun.fns.is(gun.__.opt.hooks.set)){
|
|
gun.__.opt.hooks.set(set.nodes, function(err, data){ // now iterate through those nodes to a persistence layer and get a callback once all are saved
|
|
if(err){ return cb.call(gun, err) }
|
|
return cb.call(gun, data);
|
|
});
|
|
} else {
|
|
root.console.log("Warning! You have no persistence layer to save to!");
|
|
return cb.call(gun);
|
|
}
|
|
next();
|
|
});
|
|
});
|
|
return gun;
|
|
}
|
|
Chain.map = function(cb, opt){
|
|
var gun = this;
|
|
opt = (Gun.obj.is(opt)? opt : (opt? {all: true} : {}));
|
|
gun.get(function(val){
|
|
cb = cb || function(){};
|
|
Gun.obj.map(val, function(val, field){ // by default it only maps over nodes
|
|
if(Gun._.meta == field){ return }
|
|
if(Gun.is.soul(val)){
|
|
gun.load(val).get(function(val){ // should map have support for blank?
|
|
cb.call(this, val, field);
|
|
});
|
|
} else {
|
|
if(!opt.all){ return } // {all: true} maps over everything
|
|
cb.call(gun, val, field);
|
|
}
|
|
});
|
|
});
|
|
return gun;
|
|
}
|
|
// Union is different than set. Set casts non-gun style of data into a gun compatible data.
|
|
// Union takes already gun compatible data and validates it for a merge.
|
|
// Meaning it is more low level, such that even set uses union internally.
|
|
Chain.union = function(prime, cb){
|
|
var tmp = {}, gun = this, context = Gun.shot();
|
|
cb = cb || function(){};
|
|
context.nodes = {};
|
|
if(!prime){
|
|
context.err = {err: Gun.log("No data to merge!")};
|
|
} else
|
|
if(Gun.is.soul.on(prime)){
|
|
tmp[prime._[Gun._.soul]] = prime;
|
|
prime = tmp;
|
|
}
|
|
if(!gun || context.err){
|
|
cb(context.err = context.err || {err: Gun.log("No gun instance!"), corrupt: true}, context);
|
|
return context;
|
|
}
|
|
if(!Gun.is.graph(prime, function(node, soul){
|
|
context.nodes[soul] = node;
|
|
})){
|
|
cb(context.err = context.err || {err: Gun.log("Invalid graph!"), corrupt: true}, context);
|
|
return context;
|
|
}
|
|
if(context.err){ return cb(context.err, context), context } // if any errors happened in the previous steps, then fail.
|
|
Gun.union(gun.__.graph, context.nodes).done(function(err, env){ // now merge prime into the graph
|
|
context.err = err || env.err;
|
|
cb(context.err, context || {});
|
|
}).change(function(delta){
|
|
if(!Gun.is.soul.on(delta)){ return }
|
|
gun.__.on(delta._[Gun._.soul]).emit(Gun.obj.copy(delta)); // this is in reaction to HAM. frozen copy here?
|
|
});
|
|
return context;
|
|
}
|
|
Chain.blank = function(blank){
|
|
var gun = this;
|
|
blank = blank || function(){};
|
|
gun.shot.next(function(next){
|
|
if(gun._.node){ // if it does indeed exist
|
|
return next(); // yet fire off the chain
|
|
}
|
|
blank.call(gun); // call blank
|
|
next(); // fire off the chain
|
|
});
|
|
return gun;
|
|
}
|
|
Chain.err = function(dud){ // WARNING: dud was depreciated.
|
|
this._.err = Gun.fns.is(dud)? dud : function(){};
|
|
return this;
|
|
}
|
|
}(Gun.chain = Gun.prototype));
|
|
;(function(Util){
|
|
Util.fns = {};
|
|
Util.fns.is = function(fn){ return (fn instanceof Function)? true : false }
|
|
Util.fns.sum = function(done){ // combine with Util.obj.map for some easy parallel async operations!
|
|
var context = {task: {}, data: {}};
|
|
context.end = function(e,v){ return done(e,v), done = function(){} };
|
|
context.add = function(fn, id){
|
|
context.task[id = id || (Gun.text.is(fn)? fn : Gun.text.random())] = false;
|
|
var each = function(err, val){
|
|
context.task[id] = true;
|
|
if(err){ (context.err = context.err || {})[id] = err }
|
|
context.data[id] = val;
|
|
if(!Gun.obj.map(context.task, function(val){ if(!val){ return true } })){ // it is true if we are NOT done yet, then invert.
|
|
done(context.err, context.data);
|
|
}
|
|
}, c = context;
|
|
return Gun.fns.is(fn)? function(){ return fn.apply({task: c.task, data: c.data, end: c.end, done: each}, arguments) } : each;
|
|
}
|
|
return context;
|
|
}
|
|
Util.bi = {};
|
|
Util.bi.is = function(b){ return (b instanceof Boolean || typeof b == 'boolean')? true : false }
|
|
Util.num = {};
|
|
Util.num.is = function(n){
|
|
return ((n===0)? true : (!isNaN(n) && !Util.bi.is(n) && !Util.list.is(n) && !Util.text.is(n))? true : false );
|
|
}
|
|
Util.text = {};
|
|
Util.text.is = function(t){ return typeof t == 'string'? true : false }
|
|
Util.text.ify = function(t){
|
|
if(Util.text.is(t)){ return t }
|
|
if(JSON){ return JSON.stringify(t) }
|
|
return (t && t.toString)? t.toString() : t;
|
|
}
|
|
Util.text.random = function(l, c){
|
|
var s = '';
|
|
l = l || 24; // you are not going to make a 0 length random number, so no need to check type
|
|
c = c || '0123456789ABCDEFGHIJKLMNOPQRSTUVWXZabcdefghiklmnopqrstuvwxyz';
|
|
while(l > 0){ s += c.charAt(Math.floor(Math.random() * c.length)); l-- }
|
|
return s;
|
|
}
|
|
Util.list = {};
|
|
Util.list.is = function(l){ return (l instanceof Array)? true : false }
|
|
Util.list.slit = Array.prototype.slice;
|
|
Util.list.sort = function(k){ // creates a new sort function based off some field
|
|
return function(A,B){
|
|
if(!A || !B){ return 0 } A = A[k]; B = B[k];
|
|
if(A < B){ return -1 }else if(A > B){ return 1 }
|
|
else { return 0 }
|
|
}
|
|
}
|
|
Util.list.map = function(l, c, _){ return Util.obj.map(l, c, _) }
|
|
Util.list.index = 1; // change this to 0 if you want non-logical, non-mathematical, non-matrix, non-convenient array notation
|
|
Util.obj = {};
|
|
Util.obj.is = function(o){ return (o instanceof Object && !Util.list.is(o) && !Util.fns.is(o))? true : false }
|
|
Util.obj.del = function(o, k){
|
|
if(!o){ return }
|
|
o[k] = null;
|
|
delete o[k];
|
|
return true;
|
|
}
|
|
Util.obj.ify = function(o){
|
|
if(Util.obj.is(o)){ return o }
|
|
try{o = JSON.parse(o);
|
|
}catch(e){o={}};
|
|
return o;
|
|
}
|
|
Util.obj.copy = function(o){ // because http://web.archive.org/web/20140328224025/http://jsperf.com/cloning-an-object/2
|
|
return !o? o : JSON.parse(JSON.stringify(o)); // is shockingly faster than anything else, and our data has to be a subset of JSON anyways!
|
|
}
|
|
Util.obj.has = function(o, t){ return Object.prototype.hasOwnProperty.call(o, t) }
|
|
Util.obj.map = function(l, c, _){
|
|
var u, i = 0, ii = 0, x, r, rr, f = Util.fns.is(c),
|
|
t = function(k,v){
|
|
if(v !== u){
|
|
rr = rr || {};
|
|
rr[k] = v;
|
|
return;
|
|
} rr = rr || [];
|
|
rr.push(k);
|
|
};
|
|
if(Util.list.is(l)){
|
|
x = l.length;
|
|
for(;i < x; i++){
|
|
ii = (i + Util.list.index);
|
|
if(f){
|
|
r = _? c.call(_, l[i], ii, t) : c(l[i], ii, t);
|
|
if(r !== u){ return r }
|
|
} else {
|
|
//if(Util.test.is(c,l[i])){ return ii } // should implement deep equality testing!
|
|
if(c === l[i]){ return ii } // use this for now
|
|
}
|
|
}
|
|
} else {
|
|
for(i in l){
|
|
if(f){
|
|
if(Util.obj.has(l,i)){
|
|
r = _? c.call(_, l[i], i, t) : c(l[i], i, t);
|
|
if(r !== u){ return r }
|
|
}
|
|
} else {
|
|
//if(a.test.is(c,l[i])){ return i } // should implement deep equality testing!
|
|
if(c === l[i]){ return i }
|
|
}
|
|
}
|
|
}
|
|
return f? rr : Util.list.index? 0 : -1;
|
|
}
|
|
Util.time = {};
|
|
Util.time.is = function(t){ return t? t instanceof Date : (+new Date().getTime()) }
|
|
}(Gun));
|
|
;Gun.next = function(){
|
|
var fn = function(cb){
|
|
if(!fn.stack || !fn.stack.length){
|
|
setImmediate(function next(n){
|
|
return (n = (fn.stack||[]).shift() || function(){}), n.back = fn.stack, fn.stack = [], n(function(){
|
|
return (fn.stack = (fn.stack||[]).concat(n.back)), next();
|
|
});
|
|
});
|
|
} if(cb){
|
|
(fn.stack = fn.stack || []).push(cb);
|
|
} return fn;
|
|
}, setImmediate = setImmediate || function(cb){return setTimeout(cb,0)}
|
|
return fn;
|
|
}
|
|
;Gun.shot=(function(){
|
|
// I hate the idea of using setTimeouts in my code to do callbacks (promises and sorts)
|
|
// as there is no way to guarantee any type of state integrity or the completion of callback.
|
|
// However, I have fallen. HAM is suppose to assure side effect free safety of unknown states.
|
|
var setImmediate = setImmediate || function(cb){setTimeout(cb,0)}
|
|
function Flow(){
|
|
var chain = new Flow.chain();
|
|
chain.$ = function(where){
|
|
(chain._ = chain._ || {})[where] = chain._[where] || [];
|
|
chain.$[where] = chain.$[where] || function(fn){
|
|
if(chain.args){
|
|
fn.apply(chain, chain.args);
|
|
} else {
|
|
(chain._[where]||[]).push(fn);
|
|
}
|
|
return chain.$;
|
|
}
|
|
chain.where = where;
|
|
return chain;
|
|
}
|
|
Gun.list.map(Array.prototype.slice.call(arguments), function(where){ chain.$(where) });
|
|
return chain.$;
|
|
}
|
|
Flow.is = function(flow){ return (Flow instanceof flow)? true : false }
|
|
;Flow.chain=(function(){
|
|
function Chain(){
|
|
if(!(this instanceof Chain)){
|
|
return new Chain();
|
|
}
|
|
}
|
|
Chain.chain = Chain.prototype;
|
|
Chain.chain.pipe = function(a,s,d,f){
|
|
var me = this
|
|
, where = me.where
|
|
, args = Array.prototype.slice.call(arguments);
|
|
setImmediate(function(){
|
|
if(!me || !me._ || !me._[where]){ return }
|
|
me.args = args;
|
|
while(0 < me._[where].length){
|
|
(me._[where].shift()||function(){}).apply(me, args);
|
|
}
|
|
// do a done? That would be nice. :)
|
|
});
|
|
return me;
|
|
}
|
|
return Chain;
|
|
}());
|
|
return Flow;
|
|
}());Gun.shot.chain.chain.fire=Gun.shot.chain.chain.pipe;
|
|
;Gun.on=(function(){
|
|
// events are fundamentally different, being synchronously 1 to N fan out,
|
|
// than req/res/callback/promise flow, which are asynchronously 1 to 1 into a sink.
|
|
function On(where){
|
|
if(where){
|
|
return (On.event = On.event || On.create())(where);
|
|
}
|
|
return On.create();
|
|
}
|
|
On.is = function(on){ return (On instanceof on)? true : false }
|
|
On.create = function(){
|
|
var chain = new On.chain();
|
|
return chain.$ = function(where){
|
|
chain.where = where;
|
|
return chain;
|
|
}
|
|
}
|
|
On.sort = Gun.list.sort('i');
|
|
;On.chain=(function(){
|
|
function Chain(){
|
|
if(!(this instanceof Chain)){
|
|
return new Chain();
|
|
}
|
|
}
|
|
Chain.chain = Chain.prototype;
|
|
Chain.chain.emit = function(what){
|
|
var me = this
|
|
, where = me.where
|
|
, args = arguments
|
|
, on = (me._ = me._ || {})[where] = me._[where] || [];
|
|
if(!(me._[where] = Gun.list.map(on, function(hear, i, map){
|
|
if(!hear || !hear.as){ return }
|
|
map(hear);
|
|
hear.as.apply(hear, args);
|
|
}))){ Gun.obj.del(on, where) }
|
|
}
|
|
Chain.chain.event = function(as, i){
|
|
if(!as){ return }
|
|
var me = this
|
|
, where = me.where
|
|
, args = arguments
|
|
, on = (me._ = me._ || {})[where] = me._[where] || []
|
|
, e = {as: as, i: i || 0, off: function(){ return !(e.as = false) }};
|
|
return on.push(e), on.sort(On.sort), e;
|
|
}
|
|
Chain.chain.once = function(as, i){
|
|
var me = this
|
|
, once = function(){
|
|
this.off();
|
|
as.apply(this, arguments)
|
|
}
|
|
return me.event(once, i)
|
|
}
|
|
return Chain;
|
|
}());
|
|
return On;
|
|
}());
|
|
;(function(schedule){ // maybe use lru-cache
|
|
schedule.waiting = [];
|
|
schedule.soonest = Infinity;
|
|
schedule.sort = Gun.list.sort('when');
|
|
schedule.set = function(future){
|
|
var now = Gun.time.is();
|
|
future = (future <= now)? 0 : (future - now);
|
|
clearTimeout(schedule.id);
|
|
schedule.id = setTimeout(schedule.check, future);
|
|
}
|
|
schedule.check = function(){
|
|
var now = Gun.time.is(), soonest = Infinity;
|
|
schedule.waiting.sort(schedule.sort);
|
|
schedule.waiting = Gun.list.map(schedule.waiting, function(wait, i, map){
|
|
if(!wait){ return }
|
|
if(wait.when <= now){
|
|
if(Gun.fns.is(wait.event)){
|
|
wait.event();
|
|
}
|
|
} else {
|
|
soonest = (soonest < wait.when)? soonest : wait.when;
|
|
map(wait);
|
|
}
|
|
}) || [];
|
|
schedule.set(soonest);
|
|
}
|
|
Gun.schedule = function(state, cb){
|
|
schedule.waiting.push({when: state, event: cb});
|
|
if(schedule.soonest < state){ return }
|
|
schedule.set(state);
|
|
}
|
|
}({}));
|
|
;(function(Serializer){
|
|
Gun.ify = function(data, cb){ // TODO: BUG: Modify lists to include HAM state
|
|
var gun = Gun.is(this)? this : {}
|
|
, nothing, context = Gun.shot();
|
|
context.nodes = {};
|
|
context.seen = [];
|
|
context.seen = [];
|
|
context('done');
|
|
cb = cb || function(){};
|
|
function ify(data, context, sub){
|
|
sub = sub || {};
|
|
sub.path = sub.path || '';
|
|
context = context || {};
|
|
context.nodes = context.nodes || {};
|
|
if((sub.simple = Gun.is.value(data)) && !(sub._ && Gun.text.is(sub.simple))){
|
|
return data;
|
|
} else
|
|
if(Gun.obj.is(data)){
|
|
var value = {}, meta = {}, seen
|
|
, err = {err: "Metadata does not support external or circular references at " + sub.path, meta: true};
|
|
context.root = context.root || value;
|
|
if(seen = ify.seen(context._seen, data)){
|
|
//console.log("seen in _", sub._, sub.path, data);
|
|
Gun.log(context.err = err);
|
|
return;
|
|
} else
|
|
if(seen = ify.seen(context.seen, data)){
|
|
//console.log("seen in data", sub._, sub.path, data);
|
|
if(sub._){
|
|
Gun.log(context.err = err);
|
|
return;
|
|
}
|
|
meta = Gun.ify.soul.call(gun, meta, seen);
|
|
return meta;
|
|
} else {
|
|
//console.log("seen nowhere", sub._, sub.path, data);
|
|
if(sub._){
|
|
context.seen.push({data: data, node: value});
|
|
} else {
|
|
value._ = {};
|
|
cb(data, context, sub, context.many.add(function(soul){
|
|
//console.log("What soul did we find?", soul || "random");
|
|
meta[Gun._.soul] = value._[Gun._.soul] = soul = Gun.is.soul.on(data) || soul || Gun.roulette();
|
|
context.nodes[soul] = value;
|
|
this.done();
|
|
}));
|
|
context.seen.push({data: data, node: value});
|
|
}
|
|
}
|
|
Gun.obj.map(data, function(val, field){
|
|
var subs = {path: sub.path? sub.path + '.' + field : field,
|
|
_: sub._ || (field == Gun._.meta)? true : false };
|
|
val = ify(val, context, subs);
|
|
//console.log('>>>>', sub.path + field, 'is', val);
|
|
if(context.err){ return true }
|
|
if(nothing === val){ return }
|
|
// TODO: check field validity
|
|
value[field] = val;
|
|
});
|
|
if(sub._){ return value }
|
|
if(!value._){ return }
|
|
return meta;
|
|
} else
|
|
if(Gun.list.is(data)){
|
|
var unique = {}, edges
|
|
, err = {err: "Arrays cause data corruption at " + sub.path, array: true}
|
|
edges = Gun.list.map(data, function(val, i, map){
|
|
val = ify(val, context, sub);
|
|
if(context.err){ return true }
|
|
if(!Gun.obj.is(val)){
|
|
Gun.log(context.err = err);
|
|
return true;
|
|
}
|
|
return Gun.obj.map(val, function(soul, field){
|
|
if(field !== Gun._.soul){
|
|
Gun.log(context.err = err);
|
|
return true;
|
|
}
|
|
if(unique[soul]){ return }
|
|
unique[soul] = 1;
|
|
map(val);
|
|
});
|
|
});
|
|
if(context.err){ return }
|
|
return edges;
|
|
} else {
|
|
context.err = {err: Gun.log("Data type not supported at " + sub.path), invalid: true};
|
|
}
|
|
}
|
|
ify.seen = function(seen, data){
|
|
// unfortunately, using seen[data] = true will cause false-positives for data's children
|
|
return Gun.list.map(seen, function(check){
|
|
if(check.data === data){ return check.node }
|
|
});
|
|
}
|
|
context.many = Gun.fns.sum(function(err){ context('done').fire(context.err, context) });
|
|
context.many.add(function(){
|
|
ify(data, context);
|
|
this.done();
|
|
})();
|
|
return context;
|
|
}
|
|
Gun.ify.state = function(nodes, now){
|
|
var context = {};
|
|
context.nodes = nodes;
|
|
context.now = now = (now === 0)? now : now || Gun.time.is();
|
|
Gun.obj.map(context.nodes, function(node, soul){
|
|
if(!node || !soul || !node._ || !node._[Gun._.soul] || node._[Gun._.soul] !== soul){
|
|
return context.err = {err: Gun.log("There is a corruption of nodes and or their souls"), corrupt: true};
|
|
}
|
|
var states = node._[Gun._.HAM] = node._[Gun._.HAM] || {};
|
|
Gun.obj.map(node, function(val, field){
|
|
if(field == Gun._.meta){ return }
|
|
val = states[field];
|
|
states[field] = (val === 0)? val : val || now;
|
|
});
|
|
});
|
|
return context;
|
|
}
|
|
Gun.ify.soul = function(to, from){
|
|
var gun = this;
|
|
to = to || {};
|
|
if(Gun.is.soul.on(from)){
|
|
to[Gun._.soul] = from._[Gun._.soul];
|
|
return to;
|
|
}
|
|
to[Gun._.soul] = Gun.roulette.call(gun);
|
|
return to;
|
|
}
|
|
}());
|
|
if(typeof window !== "undefined"){
|
|
window.Gun = Gun;
|
|
} else {
|
|
module.exports = Gun;
|
|
}
|
|
var root = this || {}; // safe for window, global, root, and 'use strict'.
|
|
root.console = root.console || {log: function(s){ return s }}; // safe for old browsers
|
|
var console = {log: Gun.log = function(s){return (Gun.log.verbose && root.console.log.apply(root.console, arguments)), s}};
|
|
}({}));
|
|
|
|
;(function(tab){
|
|
if(!this.Gun){ return }
|
|
if(!window.JSON){ throw new Error("Include JSON first: ajax.cdnjs.com/ajax/libs/json2/20110223/json2.js") } // for old IE use
|
|
Gun.on('opt').event(function(gun, opt){
|
|
window.tab = tab; // for debugging purposes
|
|
opt = opt || {};
|
|
tab.headers = opt.headers || {};
|
|
tab.headers['gun-sid'] = tab.headers['gun-sid'] || Gun.text.random();
|
|
tab.prefix = tab.prefix || opt.prefix || 'gun/';
|
|
tab.prekey = tab.prekey || opt.prekey || '';
|
|
tab.prenode = tab.prenode || opt.prenode || '_/nodes/';
|
|
tab.load = tab.load || function(key, cb, opt){
|
|
if(!key){ return }
|
|
cb = cb || function(){};
|
|
opt = opt || {};
|
|
opt.url = opt.url || {};
|
|
opt.headers = tab.headers;
|
|
if(key[Gun._.soul]){
|
|
opt.url.query = key;
|
|
} else {
|
|
opt.url.pathname = '/' + key;
|
|
}
|
|
Gun.log("gun load", key);
|
|
(function local(key, cb){
|
|
var node, lkey = key[Gun._.soul]? tab.prefix + tab.prenode + key[Gun._.soul]
|
|
: tab.prefix + tab.prekey + key
|
|
if((node = store.get(lkey)) && node[Gun._.soul]){ return local(node, cb) }
|
|
if(cb.node = node){ setTimeout(function(){cb(null, node)},0) }
|
|
}(key, cb));
|
|
Gun.obj.map(gun.__.opt.peers, function(peer, url){
|
|
request(url, null, function(err, reply){
|
|
Gun.log('via', url, key, reply.body);
|
|
if(err || !reply || (err = reply.body && reply.body.err)){
|
|
cb({err: Gun.log(err || "Error: Load failed through " + url) });
|
|
} else {
|
|
if(!key[Gun._.soul] && Gun.is.soul(reply.body)){
|
|
var meta = {};
|
|
meta[Gun._.soul] = Gun.is.soul(reply.body);
|
|
store.set(tab.prefix + tab.prekey + key, meta);
|
|
}
|
|
if(cb.node){ return gun.__.on(Gun.is.soul(reply.body)).emit(reply.body) }
|
|
cb(null, reply.body);
|
|
}
|
|
}, opt);
|
|
cb.peers = true;
|
|
}); tab.peers(cb);
|
|
}
|
|
tab.key = function(key, soul, cb){
|
|
var meta = {};
|
|
meta[Gun._.soul] = soul = Gun.text.is(soul)? soul : (soul||{})[Gun._.soul];
|
|
if(!soul){ return cb({err: Gun.log("No soul!")}) }
|
|
store.set(tab.prefix + tab.prekey + key, meta);
|
|
Gun.obj.map(gun.__.opt.peers, function(peer, url){
|
|
request(url, meta, function(err, reply){
|
|
if(err || !reply || (err = reply.body && reply.body.err)){
|
|
// tab.key(key, soul, cb); // naive implementation of retry TODO: BUG: need backoff and anti-infinite-loop!
|
|
cb({err: Gun.log(err || "Error: Key failed to be made on " + url) });
|
|
} else {
|
|
cb(null, reply.body);
|
|
}
|
|
}, {url: {pathname: '/' + key }, headers: tab.headers});
|
|
cb.peers = true;
|
|
}); tab.peers(cb);
|
|
}
|
|
tab.set = tab.set || function(nodes, cb){
|
|
cb = cb || function(){};
|
|
// TODO: batch and throttle later.
|
|
// tab.store.set(cb.id = 'send/' + Gun.text.random(), nodes); // TODO: store SENDS until SENT.
|
|
Gun.obj.map(nodes, function(node, soul){
|
|
if(!gun || !gun.__ || !gun.__.graph || !gun.__.graph[soul]){ return }
|
|
store.set(tab.prefix + tab.prenode + soul, gun.__.graph[soul]);
|
|
});
|
|
Gun.obj.map(gun.__.opt.peers, function(peer, url){
|
|
request(url, nodes, function(err, reply){
|
|
if(err || !reply || (err = reply.body && reply.body.err)){
|
|
return cb({err: Gun.log(err || "Error: Set failed on " + url) });
|
|
} else {
|
|
cb(null, reply.body);
|
|
}
|
|
}, {headers: tab.headers});
|
|
cb.peers = true;
|
|
}); tab.peers(cb);
|
|
Gun.obj.map(nodes, function(node, soul){
|
|
gun.__.on(soul).emit(node);
|
|
});
|
|
}
|
|
tab.peers = function(cb){
|
|
if(cb && !cb.peers){ // there are no peers! this is a local only instance
|
|
setTimeout(function(){console.log("Warning! You have no peers to connect to!");cb()},1);
|
|
}
|
|
}
|
|
tab.set.defer = {};
|
|
request.createServer(function(req, res){
|
|
// Gun.log("client server received request", req);
|
|
if(!req.body){ return }
|
|
if(Gun.is.node(req.body) || Gun.is.graph(req.body)){
|
|
gun.union(req.body); // TODO: BUG? Interesting, this won't update localStorage because .set isn't called?
|
|
}
|
|
});
|
|
gun.__.opt.hooks.load = gun.__.opt.hooks.load || tab.load;
|
|
gun.__.opt.hooks.set = gun.__.opt.hooks.set || tab.set;
|
|
gun.__.opt.hooks.key = gun.__.opt.hooks.key || tab.key;
|
|
});
|
|
var store = (function(){
|
|
function s(){}
|
|
var store = window.localStorage || {setItem: function(){}, removeItem: function(){}, getItem: function(){}};
|
|
s.set = function(key, val){ return store.setItem(key, Gun.text.ify(val)) }
|
|
s.get = function(key){ return Gun.obj.ify(store.getItem(key)) }
|
|
s.del = function(key){ return store.removeItem(key) }
|
|
return s;
|
|
}());
|
|
var request = (function(){
|
|
function r(base, body, cb, opt){
|
|
opt = opt || (base.length? {base: base} : base);
|
|
opt.base = opt.base || base;
|
|
opt.body = opt.body || body;
|
|
if(!opt.base){ return }
|
|
r.transport(opt, cb);
|
|
}
|
|
r.createServer = function(fn){ (r.createServer = fn).on = true }
|
|
r.transport = function(opt, cb){
|
|
//Gun.log("TRANSPORT:", opt);
|
|
if(r.ws(opt, cb)){ return }
|
|
r.jsonp(opt, cb);
|
|
}
|
|
r.ws = function(opt, cb){
|
|
var ws = window.WebSocket || window.mozWebSocket || window.webkitWebSocket;
|
|
if(!ws){ return }
|
|
if(ws = r.ws.peers[opt.base]){
|
|
if(!ws.readyState){ return setTimeout(function(){ r.ws(opt, cb) },10), true }
|
|
var 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 || {};
|
|
r.ws.cbs[req.headers['ws-rid'] = 'WS' + (+ new Date()) + '.' + Math.floor((Math.random()*65535)+1)] = function(err,res){
|
|
delete r.ws.cbs[req.headers['ws-rid']];
|
|
cb(err,res);
|
|
}
|
|
ws.send(JSON.stringify(req));
|
|
return true;
|
|
}
|
|
if(ws === false){ return }
|
|
ws = r.ws.peers[opt.base] = new WebSocket(opt.base.replace('http','ws'));
|
|
ws.onopen = function(o){ r.ws(opt, cb) };
|
|
ws.onclose = function(c){
|
|
if(!c){ return }
|
|
if(1006 === c.code){ // websockets cannot be used
|
|
ws = r.ws.peers[opt.base] = false;
|
|
r.transport(opt, cb);
|
|
return;
|
|
}
|
|
ws = r.ws.peers[opt.base] = null; // this will make the next request try to reconnect
|
|
};
|
|
ws.onmessage = function(m){
|
|
if(!m || !m.data){ return }
|
|
var res;
|
|
try{res = JSON.parse(m.data);
|
|
}catch(e){ return }
|
|
if(!res){ return }
|
|
res.headers = res.headers || {};
|
|
if(res.headers['ws-rid']){ return (r.ws.cbs[res.headers['ws-rid']]||function(){})(null, res) }
|
|
Gun.log("We have a pushed message!", res);
|
|
if(res.body){ r.createServer(res, function(){}) } // emit extra events.
|
|
};
|
|
ws.onerror = function(e){ Gun.log(e); };
|
|
return true;
|
|
}
|
|
r.ws.peers = {};
|
|
r.ws.cbs = {};
|
|
r.jsonp = function(opt, cb){
|
|
//Gun.log("jsonp send", opt);
|
|
r.jsonp.ify(opt, function(url){
|
|
//Gun.log(url);
|
|
if(!url){ return }
|
|
r.jsonp.send(url, function(reply){
|
|
//Gun.log("jsonp reply", reply);
|
|
cb(null, reply);
|
|
r.jsonp.poll(opt, reply);
|
|
}, opt.jsonp);
|
|
});
|
|
}
|
|
r.jsonp.send = function(url, cb, id){
|
|
var js = document.createElement('script');
|
|
js.src = url;
|
|
window[js.id = id] = function(res){
|
|
cb(res);
|
|
cb.id = js.id;
|
|
js.parentNode.removeChild(js);
|
|
window[cb.id] = null; // TODO! BUG: This needs to handle chunking!
|
|
try{delete window[cb.id];
|
|
}catch(e){}
|
|
}
|
|
js.async = true;
|
|
document.getElementsByTagName('head')[0].appendChild(js);
|
|
return js;
|
|
}
|
|
r.jsonp.poll = function(opt, res){
|
|
if(!opt || !opt.base || !res || !res.headers || !res.headers.poll){ return }
|
|
(r.jsonp.poll.s = r.jsonp.poll.s || {})[opt.base] = r.jsonp.poll.s[opt.base] || setTimeout(function(){ // TODO: Need to optimize for Chrome's 6 req limit?
|
|
//Gun.log("polling again");
|
|
var o = {base: opt.base, headers: {pull: 1}};
|
|
r.each(opt.headers, function(v,i){ o.headers[i] = v })
|
|
r.jsonp(o, function(err, reply){
|
|
delete r.jsonp.poll.s[opt.base];
|
|
while(reply.body && reply.body.length && reply.body.shift){ // we're assuming an array rather than chunk encoding. :(
|
|
var res = reply.body.shift();
|
|
//Gun.log("-- go go go", res);
|
|
if(res && res.body){ r.createServer(res, function(){}) } // emit extra events.
|
|
}
|
|
});
|
|
}, res.headers.poll);
|
|
}
|
|
r.jsonp.ify = function(opt, cb){
|
|
var uri = encodeURIComponent, q = '?';
|
|
if(opt.url && opt.url.pathname){ q = opt.url.pathname + q; }
|
|
q = opt.base + q;
|
|
r.each((opt.url||{}).query, function(v, i){ q += uri(i) + '=' + uri(v) + '&' });
|
|
if(opt.headers){ q += uri('`') + '=' + uri(JSON.stringify(opt.headers)) + '&' }
|
|
if(r.jsonp.max < q.length){ return cb() }
|
|
q += uri('jsonp') + '=' + uri(opt.jsonp = 'P'+Math.floor((Math.random()*65535)+1));
|
|
if(opt.body){
|
|
q += '&';
|
|
var w = opt.body, wls = function(w,l,s){
|
|
return uri('%') + '=' + uri(w+'-'+(l||w)+'/'+(s||w)) + '&' + uri('$') + '=';
|
|
}
|
|
if(typeof w != 'string'){
|
|
w = JSON.stringify(w);
|
|
q += uri('^') + '=' + uri('json') + '&';
|
|
}
|
|
w = uri(w);
|
|
var i = 0, l = w.length
|
|
, s = r.jsonp.max - (q.length + wls(l.toString()).length);
|
|
if(s < 0){ return cb() }
|
|
while(w){
|
|
cb(q + wls(i, (i = i + s), l) + w.slice(0, i));
|
|
w = w.slice(i);
|
|
}
|
|
} else {
|
|
cb(q);
|
|
}
|
|
}
|
|
r.jsonp.max = 2000;
|
|
r.each = function(obj, cb){
|
|
if(!obj || !cb){ return }
|
|
for(var i in obj){
|
|
if(obj.hasOwnProperty(i)){
|
|
cb(obj[i], i);
|
|
}
|
|
}
|
|
}
|
|
return r;
|
|
}());
|
|
}({}));
|