mirror of
https://github.com/amark/gun.git
synced 2025-03-30 15:08:33 +00:00
559 lines
19 KiB
JavaScript
559 lines
19 KiB
JavaScript
;(function(own){
|
|
function Gun(opt){
|
|
var gun = this;
|
|
if(!Gun.is(gun)){
|
|
return new Gun(opt);
|
|
}
|
|
gun.init(opt);
|
|
}
|
|
Gun.is = function(gun){ return (gun instanceof Gun)? true : false }
|
|
Gun._ = {};
|
|
Gun.chain = Gun.prototype;
|
|
Gun.chain._ = {};
|
|
Gun.chain._.opt = {};
|
|
Gun.chain._.nodes = {};
|
|
Gun.chain._.chain = {};
|
|
Gun.chain._.trace = [];
|
|
Gun.chain._.indices = {};
|
|
Gun.chain.init = function(opt, stun){ // idempotently update or set options
|
|
var gun = this;
|
|
gun._.events = gun._.events || Gun.on.split(); // we may not want it global for each gun instance?
|
|
gun._.events.trace = gun._.events.trace || 0;
|
|
gun._.events.at = gun._.events.at || 0;
|
|
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.hook = gun._.opt.hook || {};
|
|
Gun.obj.map(opt.hook, function(h, f){
|
|
if(!Gun.fns.is(h)){ return }
|
|
gun._.opt.hook[f] = h;
|
|
});
|
|
if(!stun){ Gun.on('init').emit(gun, opt) }
|
|
return gun;
|
|
}
|
|
Gun.chain.load = function(index, cb, opt){
|
|
var gun = this;
|
|
cb = cb || function(){};
|
|
if(cb.node = gun._.indices[index]){ // set this to the current node, too!
|
|
console.log("from gun"); // remember to do all the same stack stuff here also!
|
|
return cb(cb.node), gun;
|
|
}
|
|
cb.fn = function(){}
|
|
gun._.index = index;
|
|
// missing: hear shots!
|
|
if(Gun.fns.is(gun._.opt.hook.load)){
|
|
gun._.opt.hook.load(index, function(err, data){
|
|
gun._.loaded = (gun._.loaded || 0) + 1; // TODO: loading should be idempotent even if we got an err or no data
|
|
if(err){ return (gun._.chain.dud||cb.fn)(err) }
|
|
if(!data){ return (gun._.chain.blank||cb.fn)() }
|
|
var nodes = {}, node;
|
|
nodes[data._[own.sym.id]] = data;// missing: transform data, merging it! NO, THIS IS DONE WRONG, do a real check.
|
|
Gun.union(gun._.nodes, nodes);
|
|
node = gun._.indices[index] = gun._.nodes[data._[own.sym.id]];
|
|
cb(node); // WARNING: Need to return a frozen copy of the object! But now the internal system is using this too... :/
|
|
gun._.events.on(gun._.events.at += 1).emit(node);
|
|
}, opt);
|
|
} else {
|
|
console.log("Warning! You have no persistence layer to load from!");
|
|
}
|
|
return gun;
|
|
}
|
|
Gun.chain.index = function(index, cb){ // TODO: Need to setImmediate if not loaded yet?
|
|
console.log("make index", index);
|
|
cb = cb || function(){};
|
|
this._.indices[index] = this._.node;
|
|
if(Gun.fns.is(this._.opt.hook.index)){
|
|
this._.opt.hook.index(index, this._.node, function(err, data){
|
|
console.log("index made", index);
|
|
if(err){ return cb(err) }
|
|
return cb(null);
|
|
});
|
|
} else {
|
|
console.log("Warning! You have no index hook!");
|
|
}
|
|
return this;
|
|
}
|
|
Gun.chain.path = function(path){ // The focal point follows the path
|
|
var gun = this;
|
|
path = path.split('.');
|
|
console.log("PATH stack trace", gun._.events.trace + 1);
|
|
gun._.events.on(gun._.events.trace += 1).event(function trace(node){
|
|
console.log("stack at", gun._.events.at);
|
|
if(!path.length){ // if the path resolves to another node, we finish here
|
|
console.log("PATH resolved with node");
|
|
gun._.events.on(gun._.events.at += 1).emit(node);
|
|
return;
|
|
}
|
|
var key = path.shift()
|
|
, val = node[key];
|
|
if(key = Gun.ify.is.id(val)){ // we might end on a link, so we must resolve
|
|
gun.load(key, trace, {id: true}).blank(function(){ }).dud(function(){ }); // TODO: Need to map these to the real blank/dud
|
|
} else {
|
|
if(path.length){ // we cannot go any further, despite the fact there is more path, which means the thing we wanted does not exist
|
|
console.log("PATH failed to resolve");
|
|
gun._.events.on(gun._.events.at += 1).emit();
|
|
} else { // we are done, and this should be the value we wanted.
|
|
console.log("PATH resolved", val);
|
|
gun._.events.on(gun._.events.at += 1).emit(val);
|
|
}
|
|
}
|
|
});
|
|
return gun;
|
|
}
|
|
Gun.chain.get = function(cb){ // WARNING! Need to return a frozen copy of the object.
|
|
var gun = this;
|
|
console.log("GET stack trace", gun._.events.trace + 1);
|
|
gun._.events.on(gun._.events.trace += 1).event(function(node){
|
|
console.log("GOT", node);
|
|
cb(node);
|
|
});
|
|
return this;
|
|
}
|
|
/*
|
|
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.
|
|
*/
|
|
Gun.chain.set = function(val, cb){
|
|
var gun = this, set;
|
|
cb = Gun.fns.is(cb)? cb : function(){};
|
|
set = Gun.ify.call(gun, val);
|
|
cb.root = set.root;
|
|
if(set.err){ return cb(set.err), gun }
|
|
set = gun.set.now(set.nodes, Gun.time.is()); // set time state on nodes?
|
|
if(set.err){ return cb(set.err), gun }
|
|
Gun.union(gun._.nodes, set.nodes); // while this maybe should return a list of the nodes that were changed, we want to send the actual delta
|
|
gun._.node = cb.root;
|
|
if(Gun.fns.is(this._.opt.hook.set)){
|
|
this._.opt.hook.set(set.nodes, function(err, data){ // now iterate through those nodes to S3 and get a callback once all are saved
|
|
console.log("gun set hook callback called");
|
|
if(err){ return cb(err) }
|
|
return cb(null);
|
|
});
|
|
} else {
|
|
console.log("Warning! You have no persistence layer to save to!");
|
|
}
|
|
return gun;
|
|
}
|
|
Gun.chain.set.now = 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, id){
|
|
if(!node || !id || !node._ || !node._[own.sym.id] || node._[own.sym.id] !== id){
|
|
return context.err = {err: "There is a corruption of nodes and or their ids", corrupt: true};
|
|
}
|
|
var states = node._[own.sym.HAM] = node._[own.sym.HAM] || {};
|
|
Gun.obj.map(node, function(val, key){
|
|
if(key == own.sym.meta){ return }
|
|
val = states[key];
|
|
states[key] = (val === 0)? val : val || now;
|
|
});
|
|
});
|
|
return context;
|
|
}
|
|
Gun.chain.match = function(){ // same as path, except using objects
|
|
return this;
|
|
}
|
|
Gun.chain.blank = function(blank){
|
|
this._.chain.blank = Gun.fns.is(blank)? blank : function(){};
|
|
return this;
|
|
}
|
|
Gun.chain.dud = function(dud){
|
|
this._.chain.dud = Gun.fns.is(dud)? dud : function(){};
|
|
return this;
|
|
}
|
|
Gun.fns = {};
|
|
Gun.fns.is = function(fn){ return (fn instanceof Function)? true : false }
|
|
Gun.bi = {};
|
|
Gun.bi.is = function(b){ return (b instanceof Boolean || typeof b == 'boolean')? true : false }
|
|
Gun.num = {};
|
|
Gun.num.is = function(n){
|
|
return ((n===0)? true : (!isNaN(n) && !Gun.bi.is(n) && !Gun.list.is(n) && !Gun.text.is(n))? true : false );
|
|
}
|
|
Gun.text = {};
|
|
Gun.text.is = function(t){ return typeof t == 'string'? true : false }
|
|
Gun.text.ify = function(t){
|
|
if(JSON){ return JSON.stringify(t) }
|
|
return (t && t.toString)? t.toString() : t;
|
|
}
|
|
Gun.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;
|
|
}
|
|
Gun.list = {};
|
|
Gun.list.is = function(l){ return (l instanceof Array)? true : false }
|
|
Gun.list.slit = Array.prototype.slice;
|
|
Gun.list.sort = function(k){ // create a new sort function
|
|
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 }
|
|
}
|
|
}
|
|
Gun.list.map = function(l, c, _){ return Gun.obj.map(l, c, _) }
|
|
Gun.list.index = 1; // change this to 0 if you want non-logical, non-mathematical, non-matrix, non-convenient array notation
|
|
Gun.obj = {};
|
|
Gun.obj.is = function(o){ return (o instanceof Object && !Gun.list.is(o) && !Gun.fns.is(o))? true : false }
|
|
Gun.obj.ify = function(o){
|
|
if(Gun.obj.is(o)){ return o }
|
|
try{o = JSON.parse(o);
|
|
}catch(e){o={}};
|
|
return o;
|
|
}
|
|
Gun.obj.has = function(o, t){ return Object.prototype.hasOwnProperty.call(o, t) }
|
|
Gun.obj.map = function(l, c, _){
|
|
var u, i = 0, ii = 0, x, r, rr, f = Gun.fns.is(c),
|
|
t = function(k,v){
|
|
if(v !== u){
|
|
rr = rr || {};
|
|
rr[k] = v;
|
|
return;
|
|
} rr = rr || [];
|
|
rr.push(k);
|
|
};
|
|
if(Gun.list.is(l)){
|
|
x = l.length;
|
|
for(;i < x; i++){
|
|
ii = (i + Gun.list.index);
|
|
if(f){
|
|
r = _? c.call(_, l[i], ii, t) : c(l[i], ii, t);
|
|
if(r !== u){ return r }
|
|
} else {
|
|
//if(gun.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(Gun.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 : Gun.list.index? 0 : -1;
|
|
}
|
|
Gun.time = {};
|
|
Gun.time.is = function(t){ return t? t instanceof Date : (+new Date().getTime()) }
|
|
Gun.on = (function(){
|
|
function On(on){
|
|
var e = On.is(this)? this : events;
|
|
return e._ = e._ || {}, e._.on = Gun.text.ify(on), e;
|
|
}
|
|
On.is = function(on){ return (on instanceof On)? true : false }
|
|
On.split = function(){ return new On() }
|
|
On.sort = Gun.list.sort('i');
|
|
On.echo = On.prototype;
|
|
On.echo.on = On;
|
|
On.echo.emit = function(what){
|
|
var on = this._.on;
|
|
if(!on){ return }
|
|
this._.events = this._.events || {};
|
|
var e = this._.events[on] = this._.events[on] || (this._.events[on] = [])
|
|
, args = arguments;
|
|
if(!(this._.events[on] = Gun.list.map(e, function(hear, i, map){
|
|
if(!hear.as){ return }
|
|
map(hear);
|
|
hear.as.apply(hear, args);
|
|
}))){ delete this._.events[on] }
|
|
}
|
|
On.echo.event = function(as, i){
|
|
var on = this._.on, e;
|
|
if(!on || !as){ return }
|
|
this._.events = this._.events || {};
|
|
on = this._.events[on] = this._.events[on] || (this._.events[on] = []);
|
|
e = {as: as, i: i || 0, off: function(){ return !(e.as = false) }};
|
|
return on.push(e), on.sort(On.sort), e;
|
|
}
|
|
On.echo.once = function(as, i){
|
|
var on = this._.on, once = function(){
|
|
this.off();
|
|
as.apply(this, arguments);
|
|
}
|
|
return this.event(once, i);
|
|
}
|
|
var events = On.split();
|
|
return On;
|
|
}());
|
|
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.union = function(graph, prime){
|
|
var context = { nodes: {}};
|
|
Gun.obj.map(prime, function(node, id){
|
|
var vertex = graph[id];
|
|
if(!vertex){ // disjoint
|
|
context.nodes[node._[own.sym.id]] = graph[node._[own.sym.id]] = node;
|
|
return;
|
|
}
|
|
Gun.HAM(vertex, node, function(){ // partial
|
|
vertex[key] = deltaValue;
|
|
vertex._[own.sym.HAM][key] = node._[own.sym.HAM][key];
|
|
});
|
|
});
|
|
}
|
|
Gun.HAM = function(current, delta, some){
|
|
function HAM(machineState, incomingState, currentState, incomingValue, currentValue){
|
|
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 states = current._[own.sym.HAM] = current._[own.sym.HAM] || {} // TODO: need to cover the state of the node itself, not just the keys?
|
|
, deltaStates = current._[own.sym.HAM];
|
|
Gun.obj.map(delta, function update(deltaValue, key){
|
|
if(!Gun.obj.has(current, key)){
|
|
some(current, key, deltaValue);
|
|
return;
|
|
}
|
|
var serverState = Gun.time.is();
|
|
// add more checks?
|
|
var state = HAM(serverState, deltaStates[key], states[key], deltaValue, current[key]);
|
|
if(state.err){
|
|
console.log(".!HYPOTHETICAL AMNESIA MACHINE ERR!.", state.err);
|
|
return;
|
|
}
|
|
if(state.state || state.quarantineState || state.current){ return }
|
|
if(state.incoming){
|
|
some(current, key, deltaValue);
|
|
return;
|
|
}
|
|
if(state.amnesiaQuarantine){
|
|
Gun.schedule(deltaStates[key], function(){
|
|
update(deltaValue, key);
|
|
});
|
|
}
|
|
});
|
|
}
|
|
;(function(schedule){
|
|
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, gun){
|
|
var gun = Gun.is(this)? this : {}
|
|
, context = {
|
|
nodes: {}
|
|
,seen: []
|
|
,_seen: []
|
|
}, nothing;
|
|
function ify(data, context, sub){
|
|
sub = sub || {};
|
|
sub.path = sub.path || '';
|
|
context = context || {};
|
|
context.nodes = context.nodes || {};
|
|
if((sub.simple = Gun.ify.is(data)) && !(sub._ && Gun.text.is(sub.simple))){
|
|
return data;
|
|
} else
|
|
if(Gun.obj.is(data)){
|
|
var value = {}, symbol = {}, 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);
|
|
context.err = err;
|
|
return;
|
|
} else
|
|
if(seen = ify.seen(context.seen, data)){
|
|
//console.log("seen in data", sub._, sub.path, data);
|
|
if(sub._){
|
|
context.err = err;
|
|
return;
|
|
}
|
|
symbol = ify.id(gun, symbol, seen);
|
|
return symbol;
|
|
} else {
|
|
//console.log("seen nowhere", sub._, sub.path, data);
|
|
if(sub._){
|
|
context.seen.push({data: data, node: value});
|
|
} else {
|
|
value._ = ify.id(gun, {}, data);
|
|
context.seen.push({data: data, node: value});
|
|
context.nodes[value._[own.sym.id]] = value;
|
|
}
|
|
}
|
|
Gun.obj.map(data, function(val, key){
|
|
var subs = {path: sub.path + key + '.', _: sub._ || (key == own.sym.meta)? true : false };
|
|
val = ify(val, context, subs);
|
|
//console.log('>>>>', sub.path + key, 'is', val);
|
|
if(context.err){ return true }
|
|
if(nothing === val){ return }
|
|
// TODO: check key validity
|
|
value[key] = val;
|
|
});
|
|
if(sub._){ return value }
|
|
if(!value._ || !value._[own.sym.id]){ return }
|
|
symbol[own.sym.id] = value._[own.sym.id];
|
|
return symbol;
|
|
} 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)){
|
|
context.err = err;
|
|
return true;
|
|
}
|
|
return Gun.obj.map(val, function(id, key){
|
|
if(key !== own.sym.id){
|
|
context.err = err;
|
|
return true;
|
|
}
|
|
if(unique[id]){ return }
|
|
unique[id] = 1;
|
|
map(val);
|
|
});
|
|
});
|
|
if(context.err){ return }
|
|
return edges;
|
|
} else {
|
|
context.err = {err: "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 }
|
|
});
|
|
}
|
|
ify.id = function(gun, to, from){
|
|
to = to || {};
|
|
if(ify.is(from)){
|
|
to[own.sym.id] = from._[own.sym.id];
|
|
return to;
|
|
}
|
|
to[own.sym.id] = Gun.roulette.call(gun);
|
|
return to;
|
|
}
|
|
ify.is = function(o){
|
|
if(o && o._ && o._[own.sym.id]){
|
|
return true;
|
|
}
|
|
}
|
|
ify(data, context);
|
|
return context;
|
|
}
|
|
Gun.ify.is = function(v){ // null, binary, number (!Infinity), text, or a ref.
|
|
if(v === null){ return true } // deletes
|
|
if(v === Infinity){ return false }
|
|
if(Gun.bi.is(v)
|
|
|| Gun.num.is(v)
|
|
|| Gun.text.is(v)){
|
|
return true; // simple values
|
|
}
|
|
var yes;
|
|
if(yes = Gun.ify.is.id(v)){
|
|
return yes;
|
|
}
|
|
return false;
|
|
}
|
|
Gun.ify.is.id = function(v){
|
|
if(Gun.obj.is(v)){
|
|
var yes;
|
|
Gun.obj.map(v, function(id, key){
|
|
if(yes){ return yes = false }
|
|
if(key === own.sym.id && Gun.text.is(id)){
|
|
yes = id;
|
|
}
|
|
});
|
|
if(yes){
|
|
return yes;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
}());
|
|
own.sym = Gun.sym = {
|
|
id: '#'
|
|
,meta: '_'
|
|
,HAM: '>'
|
|
}
|
|
if(typeof window !== "undefined"){
|
|
window.Gun = Gun;
|
|
} else {
|
|
module.exports = Gun;
|
|
}
|
|
}({})); |