From 9efb1ee4a237d9f8d72c7d457aa1da5a7f5258d8 Mon Sep 17 00:00:00 2001 From: Victor Grishchenko Date: Sat, 1 Oct 2016 11:24:53 +0500 Subject: [PATCH 01/51] introduce OpStream URLs --- swarm-protocol/src/Spec.js | 4 +++ swarm-syncable/src/OpStream.js | 45 +++++++++++++++++++++++++++++- swarm-syncable/test/00_OpStream.js | 9 ++++++ 3 files changed, 57 insertions(+), 1 deletion(-) diff --git a/swarm-protocol/src/Spec.js b/swarm-protocol/src/Spec.js index 8ae7f31..40b4d74 100644 --- a/swarm-protocol/src/Spec.js +++ b/swarm-protocol/src/Spec.js @@ -115,6 +115,10 @@ class Spec { return this.Type.value; } + get clazz () { + return this.Type.value; + } + get scope () { return this.Name.origin; } diff --git a/swarm-syncable/src/OpStream.js b/swarm-syncable/src/OpStream.js index 76d3098..f192432 100644 --- a/swarm-syncable/src/OpStream.js +++ b/swarm-syncable/src/OpStream.js @@ -132,6 +132,21 @@ class OpStream { return ret; } + poll () { + let ret; + if (!this._lstn) { + } else if (this._lstn.constructor===Array && + this._lstn[0].constructor===Op) { + ret = this._lstn.shift(); + if (!this._lstn.length) + this._lstn = null; + } else if (this._lstn.constructor===Op) { + ret = this._lstn; + this._lstn = null; + } + return ret; + } + /** by default, an echo stream */ offer (op) { if (this._debug) @@ -194,6 +209,15 @@ class OpStream { callback("not implemented", null); } + static connect (url, options) { + const m = /^([\w\-]+)(\+[\w\-]+)*:/.exec(url); + if (!m) throw new Error("invalid url") + const top_proto = m[1]; + const fn = OpStream._URL_HANDLERS[top_proto]; + if (!fn) throw new Error('unknown protocol: '+top_proto); + return new fn(url, options); + } + } OpStream.MUTATIONS = "^.on.off.error.~"; @@ -202,9 +226,28 @@ OpStream.STATES = ".~"; OpStream.ENOUGH = Symbol('enough'); OpStream.OK = Symbol('ok'); OpStream.SLOW_DOWN = Symbol('slow'); // TODO relay backpressure - +OpStream._URL_HANDLERS = Object.create(null); module.exports = OpStream; +class ZeroOpStream extends OpStream { + + constructor (url, options) { + super(); + this.ops = []; + const m = /([\w\-\+]+):(\/\/)?(\w+)/.exec(url); + if (m) + OpStream.QUEUES[m[3]] = this; + } + + offer (op) { + this.ops.push(op); + } + +} +OpStream.QUEUES = Object.create(null); +OpStream._URL_HANDLERS['0'] = ZeroOpStream; + + class Filter { constructor (string, callback, once) { diff --git a/swarm-syncable/test/00_OpStream.js b/swarm-syncable/test/00_OpStream.js index ab86350..ad68962 100644 --- a/swarm-syncable/test/00_OpStream.js +++ b/swarm-syncable/test/00_OpStream.js @@ -160,4 +160,13 @@ tape ('syncable.00.C op stream - queue', function (t) { t.equals(count, 3); t.equals(tail, 1); t.end(); +}); + +tape ('syncable.00.D op stream URL', function (t) { + const zero = OpStream.connect('0://name'); + t.ok(zero===OpStream.QUEUES.name); + zero.offer(Op.NON_SPECIFIC_NOOP); + t.equals(zero.ops.length, 1); + t.ok(zero.ops[0]===Op.NON_SPECIFIC_NOOP); + t.end(); }); \ No newline at end of file From ef946f2cd2922ea65d394937eebe3cf1c61e0862 Mon Sep 17 00:00:00 2001 From: Victor Grishchenko Date: Sat, 1 Oct 2016 12:57:28 +0500 Subject: [PATCH 02/51] regex-based URL parsing --- swarm-syncable/src/URL.js | 48 +++++++++++++++++++++++++++++++++++ swarm-syncable/test/0A_URL.js | 19 ++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 swarm-syncable/src/URL.js create mode 100644 swarm-syncable/test/0A_URL.js diff --git a/swarm-syncable/src/URL.js b/swarm-syncable/src/URL.js new file mode 100644 index 0000000..4da5ac6 --- /dev/null +++ b/swarm-syncable/src/URL.js @@ -0,0 +1,48 @@ +"use strict"; + +/** node url lib 0.7KLoC and that is too much. We support a limited subset of + * URL syntax using regex-based parsing */ +class URL { + + constructor (url) { + const m = URL.RE_URI.exec(url); + if (!m) + throw new Error('invalid URL syntax'); + this.url = m[0]; + this.scheme = m[1]; + this.creds = m[2]; + this.replica = undefined; + this.password = undefined; + this.hostname = m[3]; + this.port = m[4] ? parseInt(m[4]) : 0; + this.host = this.hostname ? + this.hostname + (this.port?':'+this.port:'') : undefined; + this.path = m[5]; + this.basename; + this.search = m[6]; + this.query; + this.hash = m[7]; + this.scheme_stack; + } + + popScheme () { + + } + +} +//[\w\-]+(?:\\+[\w\-]+)* +URL.RE_URI = new RegExp( + "(?:(\\w+):)" + // scheme + "(?://" + + "(?:([^/?#\\s]*)@)?" + // credentials + "((?:[^/?#:@\\s]+\\.)*[^/?#:@\\s]+)" + // domain + "(?::([0-9]+))?" + // port + ")" + + "(/[^?#'\"\\s]*)?" + // path + "(?:\\?([^'\"#\\s]*))?" + // query + "(?:#(\\S*))?", // fragment + "gi" +); + + +module.exports = URL; \ No newline at end of file diff --git a/swarm-syncable/test/0A_URL.js b/swarm-syncable/test/0A_URL.js new file mode 100644 index 0000000..17d23b6 --- /dev/null +++ b/swarm-syncable/test/0A_URL.js @@ -0,0 +1,19 @@ +"use strict"; +let tape = require('tap').test; +let swarm = require('swarm-protocol'); +let URL = require('../src/URL'); + +tape ('syncable.0A.A OpStream URL - basic syntax', function (t) { + + const url1 = new URL('ws://host.com:1234/path?query#hash'); + t.equals(url1.scheme,"ws"); + t.equals(url1.host,"host.com:1234"); + t.equals(url1.hostname,"host.com"); + t.equals(url1.port,1234); + t.equals(url1.path,"/path"); + t.equals(url1.search,"query"); + t.equals(url1.hash,"hash"); + //t.equals(url1.,""); + t.end(); + +}); \ No newline at end of file From 6ea9839751ecbe74ea1bb5aec30a4c5e96e6fd01 Mon Sep 17 00:00:00 2001 From: Victor Grishchenko Date: Sat, 1 Oct 2016 14:11:16 +0500 Subject: [PATCH 03/51] URL to support protocol stacking --- swarm-syncable/src/URL.js | 39 +++++++++++++++++++++++++++-------- swarm-syncable/test/0A_URL.js | 26 ++++++++++++++++++++--- 2 files changed, 53 insertions(+), 12 deletions(-) diff --git a/swarm-syncable/src/URL.js b/swarm-syncable/src/URL.js index 4da5ac6..a6425b4 100644 --- a/swarm-syncable/src/URL.js +++ b/swarm-syncable/src/URL.js @@ -5,11 +5,12 @@ class URL { constructor (url) { + URL.RE_URI.lastIndex = 0; const m = URL.RE_URI.exec(url); if (!m) throw new Error('invalid URL syntax'); this.url = m[0]; - this.scheme = m[1]; + this.scheme = m[1].split('+'); this.creds = m[2]; this.replica = undefined; this.password = undefined; @@ -22,25 +23,45 @@ class URL { this.search = m[6]; this.query; this.hash = m[7]; - this.scheme_stack; } - popScheme () { + get protocol () { + return this.scheme.join('+'); + } + + clone () { + return new URL(this.toString()); + } + eq (url) { + return this.toString() === url.toString(); + } + + toString () { + let ret = this.protocol+':'; + if (this.host) + ret += '//' + (this.creds?this.creds+'@':'') + this.host; + if (this.path) + ret += this.path; + if (this.search) + ret += '?' + this.search; + if (this.hash) + ret += '#' + this.hash; + return ret; } } -//[\w\-]+(?:\\+[\w\-]+)* + URL.RE_URI = new RegExp( - "(?:(\\w+):)" + // scheme + "^(?:([\\w\\-]+(?:\\+[\\w\\-]+)*):)" + // scheme "(?://" + - "(?:([^/?#\\s]*)@)?" + // credentials - "((?:[^/?#:@\\s]+\\.)*[^/?#:@\\s]+)" + // domain - "(?::([0-9]+))?" + // port + "(?:([^/?#\\s]*)@)?" + // credentials + "((?:[^/?#:@\\s]+\\.)*[^/?#:@\\s]+)" + // domain + "(?::([0-9]+))?" + // port ")" + "(/[^?#'\"\\s]*)?" + // path "(?:\\?([^'\"#\\s]*))?" + // query - "(?:#(\\S*))?", // fragment + "(?:#(\\S*))?$", // fragment "gi" ); diff --git a/swarm-syncable/test/0A_URL.js b/swarm-syncable/test/0A_URL.js index 17d23b6..d1ca31a 100644 --- a/swarm-syncable/test/0A_URL.js +++ b/swarm-syncable/test/0A_URL.js @@ -5,8 +5,9 @@ let URL = require('../src/URL'); tape ('syncable.0A.A OpStream URL - basic syntax', function (t) { - const url1 = new URL('ws://host.com:1234/path?query#hash'); - t.equals(url1.scheme,"ws"); + const url_str = 'ws://host.com:1234/path?query#hash'; + const url1 = new URL(url_str); + t.equals(url1.protocol,"ws"); t.equals(url1.host,"host.com:1234"); t.equals(url1.hostname,"host.com"); t.equals(url1.port,1234); @@ -14,6 +15,25 @@ tape ('syncable.0A.A OpStream URL - basic syntax', function (t) { t.equals(url1.search,"query"); t.equals(url1.hash,"hash"); //t.equals(url1.,""); + + t.equals(url1.toString(), url_str); + + t.end(); + +}); + +tape ('syncable.0A.B OpStream URL - scheme nesting', function (t) { + + const url = new URL('swarm+fs+ws://host.com:1234/path?query#hash'); + t.equals(url.protocol,"swarm+fs+ws"); + t.deepEquals(url.scheme, ["swarm","fs", "ws"]); + + url.scheme.shift(); + url.hash = undefined; + t.equals(url.toString(), 'fs+ws://host.com:1234/path?query'); + + t.ok( url.eq(url.clone()) ); + t.end(); -}); \ No newline at end of file +}); From da812c2ea4185504f5eb049e977d6aad7a19fd92 Mon Sep 17 00:00:00 2001 From: Victor Grishchenko Date: Sun, 2 Oct 2016 01:20:23 +0500 Subject: [PATCH 04/51] the rising sun OpStream scheme --- swarm-syncable/src/OpStream.js | 319 +++++++++++++++-------------- swarm-syncable/test/00_OpStream.js | 41 ++-- 2 files changed, 184 insertions(+), 176 deletions(-) diff --git a/swarm-syncable/src/OpStream.js b/swarm-syncable/src/OpStream.js index f192432..f142b36 100644 --- a/swarm-syncable/src/OpStream.js +++ b/swarm-syncable/src/OpStream.js @@ -1,7 +1,10 @@ "use strict"; -let swarm = require('swarm-protocol'); -let Spec = swarm.Spec; -let Op = swarm.Op; +const swarm = require('swarm-protocol'); +const Spec = swarm.Spec; +const Op = swarm.Op; +const URL = require('./URL'); + +const MUTE=0, ONE_LSTN=1, MANY_LSTN=2, PENDING=3; /** * @@ -14,72 +17,92 @@ class OpStream { this._debug = null; } + _lstn_state () { + if (this._lstn===null) + return MUTE; + else if (this._lstn._apply) + return ONE_LSTN; + else if (this._lstn.length===0 || this._lstn[0].constructor===Op) + return PENDING; + else if (this._lstn[0]._apply) + return MANY_LSTN; + else + throw new Error('invalid _lstn'); + } /** add a new listener - * @param {String} event - a specifier filter, e.g. ".on.off" - * @param {Function} callback - a callback function - * @param {Boolean} once */ - on (event, callback, once) { - if (event && event.constructor===Function) { // normalize - callback = event; - event = ''; - } - let filter = new Filter(event, callback, once); - if (this._lstn===null) { - this._lstn = filter; - this._start(); - } else if (this._lstn.constructor===Op) { - let op = this._lstn; - this._lstn = filter; - this._emit(op); - } else if (this._lstn.constructor===Filter) { - this._lstn = [this._lstn, filter]; - } else if (this._lstn.constructor===Array) { - if (this._lstn[0].constructor===Op) { - let ops = this._lstn; - this._lstn = filter; + * @param {OpStream} opstream - the downstream + */ + on (opstream) { + if (opstream.constructor===Function) + opstream = new CallbackOpStream(opstream); + if (!opstream._apply || opstream._apply.constructor!==Function) + throw new Error('opstreams only'); + switch (this._lstn_state()) { + case MUTE: + this._lstn = opstream; + break; + case ONE_LSTN: + this._lstn = [this._lstn, opstream]; + break; + case MANY_LSTN: + this._lstn.push(opstream); + break; + case PENDING: + const ops = this._lstn; + this._lstn = opstream; this._emitAll(ops); - } else { - this._lstn.push(filter); - } - } else { - throw new Error('invalid listeners list'); + break; } + return opstream; + } + + onMatch (filter, opstream) { + if (opstream.constructor===Function) + opstream = new CallbackOpStream(opstream); + const fs = new FilterOpStream(filter); + fs.on(opstream); + return this.on(fs); + } + + onceMatch (filter, callback) { + const fs = new FilterOpStream(filter); + const opstream = new CallbackOpStream(callback, true); + fs.on(opstream); + return this.on(fs); + } + + once (callback) { + const opstream = new CallbackOpStream(callback, true); + return this.on(opstream); } /** internal callback; triggered on a first listener added iff nothing * has been emitted yet. */ _start () {} - once (event, callback) { - if (event.constructor===Function) { - callback = event; - event = ''; - } - this.on(event, callback, true); - } - /** remove listener(s) */ - off (event, callback) { - if (event && event.constructor===Function) { - callback = event; - event = undefined; - } - - if (this._lstn===null) { - return; - } else if (this._lstn.constructor===Filter) { - if (this._lstn.callback===callback) - this._lstn = null; - } else if (this._lstn.constructor===Array) { - this._lstn = this._lstn.filter( f => - f.constructor!==Filter || - (callback && f.callback !== callback) || - (event!==undefined && f.toString()!==event) - ); - if (this._lstn.length===0) - this._lstn = null; + off (opstream) { + if (!opstream._apply) + throw new Error("can only add/remove opstreams"); + switch (this._lstn_state()) { + case MUTE: + break; + case ONE_LSTN: + if (this._lstn === opstream) + this._lstn = null; + break; + case MANY_LSTN: + const i = this._lstn.indexOf(opstream); + if (i!==-1) + this._lstn.splice(i, 1); + if (this._lstn.length===0) + this._lstn = null; + break; + case PENDING: + break; } + return opstream; } /** Emit a new op to all the interested listeners. @@ -89,62 +112,51 @@ class OpStream { _emit (op) { if (this._debug) console.warn('{'+this._debug+'\t'+(op?op.toString():'[EOF]')); - if (this._lstn===null) { - this._lstn = op; - } else if (this._lstn.constructor===Filter) { - if (this._lstn.offer(op, this)===OpStream.ENOUGH) - this._lstn = null; - } else if (this._lstn.constructor===Array) { - if (this._lstn[0].constructor===Op) { - this._lstn.push(op); - } else { - let ejects = []; - this._lstn.forEach( f => - f.offer(op, this)===OpStream.ENOUGH && ejects.push(f) - ); - if (ejects.length) { - this._lstn = this._lstn.filter(f=>ejects.indexOf(f)===-1); - if (this._lstn.length===0) - this._lstn = null; + switch (this._lstn_state()) { + case MUTE: + break; + case ONE_LSTN: + if (this._lstn._apply(op)===OpStream.ENOUGH) + this._lstn = null; + break; + case MANY_LSTN: + let ejects = 0, l = this._lstn; + for(let i=0; i0) { + l = l.filter( x => x!==null ); + this._lstn = l.length ? l : null; } - } - } else if (this._lstn.constructor===Op) { - this._lstn = [this._lstn, op]; + break; + case PENDING: + this._lstn.push(op); + break; } - // FIXME null is delivered to all listeners, _lstn:=null - // test: emit op, end(), then on() + if (op===null) + this._lstn = null; } _emitAll (ops) { ops.forEach(op => this._emit(op)); } - spill () { - let ret = []; - if (!this._lstn) { - } else if (this._lstn.constructor===Array && - this._lstn[0].constructor===Op) { + pollAll () { + let ret = null; + if (this._lstn_state()===PENDING) { ret = this._lstn; - } else if (this._lstn.constructor===Op) { - ret = [this._lstn]; + this._lstn = null; } - this._lstn = null; return ret; } poll () { - let ret; - if (!this._lstn) { - } else if (this._lstn.constructor===Array && - this._lstn[0].constructor===Op) { - ret = this._lstn.shift(); - if (!this._lstn.length) - this._lstn = null; - } else if (this._lstn.constructor===Op) { - ret = this._lstn; - this._lstn = null; - } - return ret; + if (this._lstn_state()===PENDING) + return this._lstn.shift(); + else + return null; } /** by default, an echo stream */ @@ -162,37 +174,23 @@ class OpStream { this.offer(null); } - /** @param {OpStream} sink */ - pipe (sink) { - this.on('', sink.offer.bind(sink)); - // TODO test - } - - connect (opstream) { - this.pipe(opstream); - opstream.pipe(this); - } - - _end () { - this._emit(null); - } - - onEnd (callback) { - this.on(null, callback); + onceEnd (callback) { + this.onceMatch(null, callback); } onHandshake (callback) { - this.on(OpStream.HANDSHAKES, callback); + this.onMatch(OpStream.HANDSHAKES, callback); } onMutation (callback) { - this.on(OpStream.MUTATIONS, callback); + this.onMatch(OpStream.MUTATIONS, callback); } onState (callback) { - this.on(OpStream.STATES, callback); + this.onMatch(OpStream.STATES, callback); } + /* _listFilters () { if (!this._filters) return ''; let list = this._up ? '*\t' + this._up.toString() : ''; @@ -201,17 +199,11 @@ class OpStream { ).join('\n'); return list; } - - /** Try creating a replacement for a closed OpStream. (Optional.) - * @param {Function} callback - invoked with 2 arguments, error message and - * a successor opstream (one must be null) */ - retry (callback) { - callback("not implemented", null); - } + */ static connect (url, options) { const m = /^([\w\-]+)(\+[\w\-]+)*:/.exec(url); - if (!m) throw new Error("invalid url") + if (!m) throw new Error("invalid url"); const top_proto = m[1]; const fn = OpStream._URL_HANDLERS[top_proto]; if (!fn) throw new Error('unknown protocol: '+top_proto); @@ -234,9 +226,9 @@ class ZeroOpStream extends OpStream { constructor (url, options) { super(); this.ops = []; - const m = /([\w\-\+]+):(\/\/)?(\w+)/.exec(url); - if (m) - OpStream.QUEUES[m[3]] = this; + this.url = new URL(url); + if (this.url.host) + OpStream.QUEUES[this.url.host] = this; } offer (op) { @@ -248,20 +240,38 @@ OpStream.QUEUES = Object.create(null); OpStream._URL_HANDLERS['0'] = ZeroOpStream; -class Filter { +class CallbackOpStream extends OpStream { + + constructor (callback, once) { + super(); + if (!callback || callback.constructor!==Function) + throw new Error('callback is not a function'); + this._callback = callback; + this._once = !!once; + } + + _apply (op) { + return this._callback(op)===OpStream.ENOUGH || this._once ? + OpStream.ENOUGH : OpStream.OK; + } + +} - constructor (string, callback, once) { - this.callback = callback; - this.negative = string && string.charAt(0)==='^'; + +class FilterOpStream extends OpStream { + + constructor (string, once) { + super(); + this._negative = string && string.charAt(0)==='^'; this._patterns = [null, null, null, null]; - this.once = once; + this._once = once; if (string===null) { //eof this._patterns = null; return; } let m = null; - Filter.reTok.lastIndex = this.negative ? 1 : 0; - while (m = Filter.reTok.exec(string)) { + FilterOpStream.reTok.lastIndex = this._negative ? 1 : 0; + while (m = FilterOpStream.reTok.exec(string)) { let quant = m[1], stamp = m[2], t = Spec.quants.indexOf(quant); if (this._patterns[t]===null) { this._patterns[t] = []; @@ -270,7 +280,7 @@ class Filter { } } - covers (op) { + matches (op) { let pns = this._patterns; if (op===null || pns===null) { return op===null && (pns===null || pns.every(p=>p===null)); @@ -281,26 +291,25 @@ class Filter { if (mine===null) continue; let its = spec._toks[t]; let bad = mine.every(stamp => !stamp.eq(its)); - if (bad) return this.negative; + if (bad) return this._negative; } - return !this.negative; + return !this._negative; + } + + _offer () { + throw new Error('not implemented'); } - offer (op, context) { - if (this.callback && this.covers(op)) { - let ret = this.callback.call(context, op, context); - if (this.once || ret === OpStream.ENOUGH) - return OpStream.ENOUGH; - if (ret && ret.constructor === Function) - this.callback = ret; - } - return OpStream.OK; + _apply (op, opstream) { + if (this.matches(op)) + this._emit(op); + return this._once || this._lstn===null ? OpStream.ENOUGH : OpStream.OK; } toString () { let p = this._patterns; if (p===null) return null; // take that (TODO) - let ret = this.negative ? '^' : ''; + let ret = this._negative ? '^' : ''; for(let q=0; q<4; q++) if (p[q]!==null) { p[q].forEach(stamp => ret+=Spec.quants[q]+stamp); @@ -310,10 +319,6 @@ class Filter { } -Filter.rsTok = '([/#!\\.])(' + swarm.Stamp.rsTok + ')'; -Filter.reTok = new RegExp(Filter.rsTok, 'g'); -OpStream.Filter = Filter; - -OpStream.TRACE = op => console.log(op.toString()); - -// TODO ENLIGHT FilterOpStream :) \ No newline at end of file +FilterOpStream.rsTok = '([/#!\\.])(' + swarm.Stamp.rsTok + ')'; +FilterOpStream.reTok = new RegExp(FilterOpStream.rsTok, 'g'); +OpStream.Filter = FilterOpStream; diff --git a/swarm-syncable/test/00_OpStream.js b/swarm-syncable/test/00_OpStream.js index ad68962..0c78a01 100644 --- a/swarm-syncable/test/00_OpStream.js +++ b/swarm-syncable/test/00_OpStream.js @@ -1,5 +1,5 @@ "use strict"; -let tape = require('tap').test; +let tape = require('tape').test; let swarm = require('swarm-protocol'); let OpStream = require('../src/OpStream'); let Op = swarm.Op; @@ -25,31 +25,31 @@ tape ('syncable.00.A echo op stream - event filtering', function (t) { // except each token may have many accepted values (OR) // Different tokens are AND'ed. - stream.on(".on", op => ons++); + stream.onMatch(".on", op => ons++); // may use stream.onHandshake(op => onoffs++) // means: ".on OR .off" - stream.on(".on.off", op => onoffs++); + stream.onMatch(".on.off", op => onoffs++); // may use stream.on(".~", op => states++) stream.onState(op => states++); // the leading ^ is a negation, i.e "NOT (.on OR .off OR ...)" - stream.on("^.on.off.error.~", op=> mutationsA++); + stream.onMatch("^.on.off.error.~", op=> mutationsA++); // exactly the same result, without the mumbo-jumbo: stream.onMutation(op=> mutationsB++); // filters database close event, ".off AND /Swarm" - stream.on("/Swarm.off", op=> unsub=true); + stream.onMatch("/Swarm.off", op=> unsub=true); // this catches a fresh subscription to #7AM0f+gritzko - stream.on("/Object#7AM0f+gritzko!0", on => { + stream.onMatch("/Object#7AM0f+gritzko!0", on => { myobj++; t.ok(on.isOn()); }); // may use stream.on ( null, () => {...} ) - stream.onEnd(nothing => { + stream.onceEnd(nothing => { t.equals(nothing, null); t.equals(ons, 1); t.equals(onoffs, 2); @@ -63,7 +63,7 @@ tape ('syncable.00.A echo op stream - event filtering', function (t) { // feed all the ops into the echo stream to trigger listeners stream.offerAll(ops); // stream.offer(null) has the same effect as stream.end() - stream.end(); + stream.offer(null); // in case you'll need to debug that, v8 is awesome: // console.log(stream._listFilters()); @@ -98,26 +98,25 @@ tape ('syncable.00.A echo op stream - listener mgmt', function (t) { let stream = new OpStream(); - let once = 0, ons = 0, first_on = false, second_on = false; + let once = 0, ons = 0, first_on = false; let total = 0, total2 = 0, before_value = 0, three=0; stream.on(op => total++); stream.on(op => total2++); - stream.once('.on', op => once++ ); - stream.on('.on', () => ons++ ); - stream.on('.on', op => { + stream.onceMatch('.on', op => once++ ); + stream.onMatch('.on', () => ons++ ); + stream.onMatch('.on', op => { first_on = true; - return op => second_on=true; }); stream.on( op => { if (op && op.value) return OpStream.ENOUGH; before_value++; }); - stream.on(function removable (op) { + const handle = stream.on(function removable (op) { three++; if (op.type=='Swarm') - stream.off(removable); + stream.off(handle); }); stream.offerAll(ops); @@ -130,7 +129,6 @@ tape ('syncable.00.A echo op stream - listener mgmt', function (t) { t.equals(before_value, 2); t.equals(three, 3); t.ok(first_on); - t.ok(second_on); t.end(); @@ -151,6 +149,7 @@ tape ('syncable.00.B op stream - filter', function (t) { tape ('syncable.00.C op stream - queue', function (t) { let stream = new OpStream(); + stream._lstn = []; let count = 0, tail = 0; stream.offer(Op.NOTHING); stream.offer(Op.NOTHING); @@ -159,14 +158,18 @@ tape ('syncable.00.C op stream - queue', function (t) { stream.offer(Op.NOTHING); t.equals(count, 3); t.equals(tail, 1); + stream._lstn = null; // only pool ops if lstn is [] + stream.offer(Op.NOTHING); + t.equals(count, 3); + t.equals(tail, 1); t.end(); }); tape ('syncable.00.D op stream URL', function (t) { - const zero = OpStream.connect('0://name'); - t.ok(zero===OpStream.QUEUES.name); + const zero = OpStream.connect('0://00.D'); + t.ok(zero===OpStream.QUEUES['00.D']); zero.offer(Op.NON_SPECIFIC_NOOP); t.equals(zero.ops.length, 1); t.ok(zero.ops[0]===Op.NON_SPECIFIC_NOOP); t.end(); -}); \ No newline at end of file +}); From 4f05282ecf737f3c4da45a6a2049371786c25bcf Mon Sep 17 00:00:00 2001 From: Victor Grishchenko Date: Mon, 3 Oct 2016 02:32:10 +0500 Subject: [PATCH 05/51] convert Client, Syncable to the rising-sun scheme (and tests) --- swarm-syncable/src/Client.js | 210 ++++++++++++++-------- swarm-syncable/src/LWWObject.js | 68 +++++--- swarm-syncable/src/Swarm.js | 14 +- swarm-syncable/src/Syncable.js | 271 ++++++++++++++++------------- swarm-syncable/src/URL.js | 38 ++-- swarm-syncable/test/01_Syncable.js | 43 ++--- swarm-syncable/test/02_Client.js | 116 +++++++----- swarm-syncable/test/0A_URL.js | 7 +- 8 files changed, 463 insertions(+), 304 deletions(-) diff --git a/swarm-syncable/src/Client.js b/swarm-syncable/src/Client.js index fa3500f..eba7a24 100644 --- a/swarm-syncable/src/Client.js +++ b/swarm-syncable/src/Client.js @@ -1,12 +1,13 @@ 'use strict'; -let swarm = require('swarm-protocol'); -let Base64x64 = swarm.Base64x64; -let Stamp = swarm.Stamp; -let Spec = swarm.Spec; -let Op = swarm.Op; -let SwarmMeta = require('./Swarm'); -let OpStream = require('./OpStream'); -let Syncable = require('./Syncable'); +const swarm = require('swarm-protocol'); +const Stamp = swarm.Stamp; +const Spec = swarm.Spec; +const Op = swarm.Op; +const SwarmMeta = require('./Swarm'); +const OpStream = require('./OpStream'); +const Syncable = require('./Syncable'); +const LWWObject = require('./LWWObject'); +const URL = require('./URL'); /** * Client is the world of actual replicated/synchronized objects of various types. @@ -29,48 +30,74 @@ class Client extends OpStream { * Create a Client given an upstream and a database id. * Replica id is granted by the upstream. * - * @param {Spec} spec - typeid spec for the database, e.g. `/Swarm#test` or + * @param {String} url - typeid spec for the database, e.g. `/Swarm#test` or * `test` or `` for the default database * @param {Object} options - local defaults and overrides for the metadata object */ - constructor (spec, options) { - super(); // TODO snapshots (Swarm 1.4) + constructor (url, options) { + super(); + this._url = new URL(url); + this._id = this._url.replica || '0'; /** syncables, API objects, the outer state */ this._syncables = Object.create(null); /** we can only init the clock once we have a meta object */ this._clock = null; - this._last_acked = Stamp.ZERO; + let next = this._url.clone(); + next.scheme.shift(); + this._upstream = OpStream.connect(next); + this._upstream.on(this); + this._unsynced = new Map(); + this._meta = this.get( + SwarmMeta.RDT.Type, + this.dbid, + state => { + this._clock = new swarm.Clock(state.scope, this._meta.filterByPrefix('Clock')); + this._id = state.scope; + this._clock.seeTimestamp(state.spec.Stamp); + } + ); + this._meta.onceSync ( + reon => this._clock.seeTimestamp(reon.spec.Stamp) + ); + if (!Syncable.defaultHost) // TODO deprecate + Syncable.defaultHost = this; + } - this._op_cb = this.onSyncableOp.bind(this); + get dbid () { + return this._url.basename; + } - if (!Syncable.multiClient && !Syncable.defaultClient) { - Syncable.defaultClient = this; - } + get origin () { + return this._clock && this._clock.origin; + } - /** database meta object */ - spec = new Spec(spec); - this._meta = new SwarmMeta(spec, null, options); // FIXME defaults - // tip the avalanche - this._meta.onceReady( on => { - this._clock = new swarm.Clock(on.scope, this._meta.filterByPrefix('Clock')); - }); - this.addSyncable(this._meta, spec.Id); + onceReady (callback) { + this._meta.onceReady(callback); } /** Inject an op. */ - offer(op) { - let obj = this._syncables[op.typeid]; - if (op.origin === this._id) { + _apply (op) { + const rdt = this._syncables[op.typeid]._rdt; + if (!op.spec.Stamp.isAbnormal() && this._clock) + this._clock.seeTimestamp(op.spec.Stamp); + if (op.isOnOff()) + this._unsynced.delete(op.spec.object); + if (op.origin === this.origin) { this._last_acked = op.spec.Stamp; - } else if (!obj) { // not intereted - if (op.name !== "off") - this.emit(new Op(op.spec.rename('off'), '')); } else { - obj.offer(op); + if (!rdt && op.name !== "off") + this._upstream.offer(new Op(op.spec.rename('off'), '')); + else + rdt._apply(op); + this._emit(op); } } + offer (op, source) { + this._upstream.offer(op); + } + close () { // let's be explicit Object.keys(this._syncables).forEach(typeid => this.removeSyncable(this._syncables[typeid])); @@ -87,38 +114,64 @@ class Client extends OpStream { /** - * Open a syncable and attach it to this replica. The state is queried from + * Attach a syncable to this replica. The state is queried from * the upstream. Till the state is received, the object is stateless. - * @param {Spec} spec - the object's /type#id + * @param {Syncable} obj - the object */ - addSyncable (spec) { - let fn = Syncable._classes[spec.type]; - if (!fn) - throw new Error('unknown syncable type '+spec.type); - let obj = new fn(null); - obj.on(this._op_cb); - obj._clock = this._clock; - obj._id = spec.id; - this._syncables[obj.typeid] = obj; - this._emit(obj.toOnOff(true)); - return obj; + //--- + + createOp (opname, opvalue, syncable) { + const stamp = this.time(); + const spec = new Spec([ syncable.Type, syncable.Id, stamp, opname ]); + const op = new Op(spec, opvalue); + syncable.apply(op); + this._emit(op); } /** * Create a syncable and attach it to this replica. Tthe existing object's * state it taken as the initial state, timestamped and broadcast. The * object gets an id (the stamp of that very op). - * @param {Syncable} obj - the syncable object + * @param {Stamp|String|Base64x64} type - the syncable object type + * @param feed_state - the initial state */ - addNewSyncable (obj) { - obj.on(this._op_cb); - obj._clock = this._clock; - obj._submit(); - this._emit(obj.toOnOff(true)); - this._syncables[obj.typeid] = obj; + create (type, feed_state) { + const fn = Syncable.getClass(type); + if (!fn) + throw new Error('unknown syncable type '+type); + const stamp = this.time(); + const spec = new Spec([ type, stamp, stamp, Op.STAMP_STATE ]); + const state = feed_state===undefined ? '' : + fn._init_state(feed_state, stamp, this._clock); + const op = new Op( spec, state ); + this._upstream.offer(op); + const rdt = new fn.RDT(op, this); + this._upstream.offer(rdt.toOnOff(true).scoped(this._id), this); + return this._syncables[spec.object] = new fn(rdt); } - removeSyncable (obj) { + /** Fetch an object by its id + * @param {Spec} spec */ + fetch (spec, on_state) { + const have = this._syncables[spec.object]; + if (have) { + on_state && on_state(); + return have; + } + const state0 = new Op( [spec.Type, spec.Id, Stamp.ZERO, Op.STAMP_STATE], '' ); + const fn = Syncable.getClass(spec.Type); + if (!fn) + throw new Error('unknown syncable type '+spec); + const rdt = new fn.RDT(state0, this); + const on = rdt.toOnOff(true).scoped(this._id); + if (on.spec.clazz==='Swarm' && this._url.password) + on._value = 'Password: '+this._url.password; // FIXME E E + this._upstream.offer(on, this); + this._unsynced.set(spec.object, 1); + return this._syncables[spec.object] = new fn(rdt, on_state); + } + + _remove_syncable (obj) { let prev = this._syncables[obj.typeid]; if (prev!==obj) { return; @@ -128,15 +181,6 @@ class Client extends OpStream { this._emit(obj.toOnOff(false)); } - onSyncableOp (op, obj) { - if (op===null) { - this.removeSyncable(obj); - } else if (this._clock && op.origin===this._clock.origin) { - this._emit(op); - } - } - - get id () { return this._clock.origin; } @@ -154,14 +198,23 @@ class Client extends OpStream { return this._clock ? this._clock.issueTimestamp() : null; } + /** @return {Swarm} */ + get meta () { + return this._meta; + } + // mark-and-sweep kind-of distributed garbage collection gc () { // NOTE objects in this.pending can NOT be gc'd } /** Get a Syncable object for of the given type, with the given id. */ - get (type, id) { - return this.getBySpec(new Spec([type, id, Stamp.ZERO, Stamp.ZERO])); + get (type, id, callback) { + if (id===undefined) { + id = type; + type = LWWObject.Type; + } + return this.fetch(new Spec([type, id, Stamp.ZERO, Stamp.ZERO]), callback); } /** Retrieve a Syncable object for a given specifier. @@ -177,24 +230,35 @@ class Client extends OpStream { return have; } - create (type) { - if (type.constructor!==Stamp) - type = new Stamp(type); - let typefn = Syncable._classes[type.value]; - if (!typefn) throw new Error('unknown type '+type.value); - return new typefn(null, this); + onSync (callback) { + if (this._unsynced.size===0) { + callback(null); + } else { + this.on(op => { // TODO .on .off + if (this._unsynced.size === 0) { + callback(op); + return OpStream.ENOUGH; + } else { + return OpStream.OK; + } + }); + } } static get (type, id) { return Syncable.defaultClient.get(type, id); } - static getBySpec (spec) { - return Syncable.defaultClient.getBySpec(spec); + static fetch (spec) { + return Syncable.defaultClient.fetch(spec); + } + + static create (type, state) { + return Syncable.defaultClient.create(type, state); } - static create (type) { - return Syncable.defaultClient.create(type); + newLWWObject (init_obj) { + return this.create(LWWObject.RDT.Type, init_obj); } } diff --git a/swarm-syncable/src/LWWObject.js b/swarm-syncable/src/LWWObject.js index 81e4e0e..65aeac3 100644 --- a/swarm-syncable/src/LWWObject.js +++ b/swarm-syncable/src/LWWObject.js @@ -8,14 +8,8 @@ let Op = swarm.Op; /** Flat LWW object: field values are either a string or a number or a reference. */ class LWWObject extends Syncable { - /** - * @param {Object} state - a {key: value} state for a new object - */ - constructor (state) { - super(); - this._values = Object.create(null); - if (state) - this.setAll(state); + static normalize_start_state (something, stamp) { + // return a string } /** @param {Object} obj - field-value map */ @@ -26,12 +20,22 @@ class LWWObject extends Syncable { forEach(key => this.set(key, obj[key])); } + static _init_state (obj, stamp, clock) { + let ret = Object.keys(obj).filter(key=>LWWObject.reFieldName.test(key)). + sort().map( key => + new Spec([Stamp.ZERO, Stamp.ZERO, clock.issueTimestamp(), key]).event + + '\t' + + JSON.stringify(obj[key]) + ); + return ret.join('\n'); + } + set (name, value) { if (value===undefined) throw new Error('need a valid JSON value'); if (!LWWObject.reFieldName.test(name)) throw new Error('need a valid Base64x64 field name'); - this._submit(name, JSON.stringify(value)); + this._offer(name, JSON.stringify(value)); } get (name) { @@ -43,8 +47,8 @@ class LWWObject extends Syncable { } StampOf (name) { - const at = this._state.at(name); - return at===-1 ? Stamp.ZERO : this._state.ops[at].spec.Stamp; + const at = this._rdt.at(name); + return at===-1 ? Stamp.ZERO : this._rdt.ops[at].spec.Stamp; } stampOf (key) { @@ -56,16 +60,20 @@ class LWWObject extends Syncable { } _rebuild (op) { - const name = op.spec.method; + const name = op ? op.spec.method : Op.METHOD_STATE; // :( if (name===Op.METHOD_STATE) { // rebuild this._values = Object.create(null); - this._state.ops.forEach(e=>{ + this._rdt.ops.forEach(e=>{ this._values[e.spec.method] = JSON.parse(e.value); }); } else if (this._version < op.spec.stamp) { this._values[name] = JSON.parse(op.value); } else { // reorder - this._values[name] = JSON.parse(this._state.get(name)); + const value = this._rdt.get(name); + if (value===undefined) + delete this._values[name]; + else + this._values[name] = JSON.parse(value); } } @@ -74,16 +82,16 @@ module.exports = LWWObject; LWWObject.reFieldName = /^[a-zA-Z][A-Za-z_0-9]{0,9}$/; -LWWObject.id = 'LWWObject'; -Syncable._classes[LWWObject.id] = LWWObject; - - /** reducer: (string, op) -> new_string */ class LWWObjectRDT extends Syncable.RDT { - constructor (state) { - super(); - this.ops = Op.parseFrame(state+'\n\n') || []; + constructor (state, host) { + super(state, host); + this.ops = this.ops || []; + } + + reset (state_op) { + this.ops = Op.parseFrame(state_op.value+'\n\n') || []; } at (name) { @@ -93,7 +101,21 @@ class LWWObjectRDT extends Syncable.RDT { return -1; } - apply (op) { + _apply (op) { + switch (op.spec.method) { // FIXME ugly + case Op.METHOD_NOOP: + case Op.METHOD_ON: + case Op.METHOD_OFF: + case Op.METHOD_STATE: + case Op.METHOD_ERROR: + break; + default: + this._set(op); + } + super._apply(op); + } + + _set (op) { op = new Op(op.spec.Event, op.value); const at = this.at(op.spec.method); if (at===-1) @@ -116,3 +138,5 @@ class LWWObjectRDT extends Syncable.RDT { } LWWObject.RDT = LWWObjectRDT; +LWWObjectRDT.Type = new Stamp('LWWObject'); +Syncable.addClass(LWWObject); diff --git a/swarm-syncable/src/Swarm.js b/swarm-syncable/src/Swarm.js index daad452..cbc1e2f 100644 --- a/swarm-syncable/src/Swarm.js +++ b/swarm-syncable/src/Swarm.js @@ -7,9 +7,6 @@ const ReplicaIdScheme = swarm.ReplicaIdScheme; /** Database metadata object. */ class Swarm extends LWWObject { - constructor (value_or_anything, host) { - super(value_or_anything, host); - } filterByPrefix (prefix) { let ret = Object.create(null); @@ -23,9 +20,14 @@ class Swarm extends LWWObject { } +class SwarmRDT extends LWWObject.RDT { + constructor (state, host) { + super(state, host); + } +} - -Swarm.id = 'Swarm'; // FIXME rename to CLASS -Syncable._classes[Swarm.id] = Swarm; +Swarm.RDT = SwarmRDT; +SwarmRDT.Type = new swarm.Stamp('Swarm'); // FIXME rename to CLASS +Syncable.addClass(Swarm); module.exports = Swarm; \ No newline at end of file diff --git a/swarm-syncable/src/Syncable.js b/swarm-syncable/src/Syncable.js index 0b3e00b..d5eab5f 100644 --- a/swarm-syncable/src/Syncable.js +++ b/swarm-syncable/src/Syncable.js @@ -21,95 +21,60 @@ class Syncable extends OpStream { /** * @constructor - * @param {String} state - the state to init a new object with - * ('' for the type's default state, null for "not retrieved yet") + * @param {Op} state_op - the state to init a new object with + * (can be the type's default state, version 0) + * @param {Client} host - the client that hosts this syncable (optional) + * @param {Function} callback - callback to invoke once the object is stateful */ - constructor (state) { + constructor (rdt, callback) { super(); - if (state===undefined) - state = ''; // new object in the default state - - /** The id of an object is typically the timestamp of the first - operation. Still, it can be any Base64 string (see swarm-stamp). */ - this._id = Stamp.zero; // string, not stamp (it's cheaper this way) /** The RDT inner state */ - this._state = state===null ? null : new this.constructor.RDT(state); - /** the clock to stamp ops with */ - this._clock = null; - /** Timestamp of the last change op. Transcendent stamps are used - * for detached objects. */ - this._version = Stamp.zero; - /** Cached "/type#id" string key */ - this._typeid = null; - - if (state!==null && Syncable.defaultHost) { - Syncable.defaultHost.addNewSyncable(this); - } + this._rdt = rdt; + rdt.on(this); + this._rebuild(); + + if (callback) + this.once(callback); + } - static setDefaultHost (host) { - Syncable.defaultHost = host; + /** @returns {Client} host */ + get host () { + return this._rdt._host; } noop () { - this._submit("0", "", this); + this._submit("0", ""); } /** Create, apply and emit a new op. * @param {String} op_name - the operation name (Base64x64, transcendent) * @param {String} op_value - the op value */ - _submit (op_name, op_value) { - if (!this._clock && this.id!=='0') - throw new Error('stateful obj, no clock'); - let stamp = this._clock ? this._clock.issueTimestamp() : Base64x64.inc(this._version); - if (!op_name) { - op_name = '~'; - op_value = this.state; - } - let spec = new Spec([ - this.type, - this._id==='0' && this._clock ? stamp : this._id, - stamp, - op_name - ]); - let op = new Op(spec, op_value); - this.offer(op); + _offer (op_name, op_value) { // FIXME BAD!!! + const stamp = this._rdt._host.time(); + const spec = new Spec([this.Type, this.Id, stamp, new Stamp(op_name)]); + const op = new Op(spec, op_value); + this._rdt.offer(op); } /** Apply an op to the object's state. * @param {Op} op - the op */ - offer (op) { - if (this._id==='0') { - this._id = op.spec.id; - this._typeid = null; - } else if ( this._id !== op.id ) { - throw new Error('not my op'); - } - if (op.spec.method===Op.METHOD_STATE) { - this._state = new this.constructor.RDT(op.value); - } else { - if (!this._state) { - let default_state = new Op(op.spec.rename('~'), ''); - this.offer(default_state); - } - this._state.apply(op); - } + _apply (op) { this._rebuild(op); - if (!op.isOnOff()) - this._version = op.spec.stamp; // after the rebuild! - if (!op.spec.Stamp.isTranscendent()) // dry run ops are not emitted - this._emit(op); + this._emit(op); } - _rebuild (op) {} + _rebuild (op) { + + } get id () { - return this._id; + return this.Id.toString(); } get Id () { - return new Stamp(this._id); + return this._rdt.Id; } /** @@ -126,11 +91,12 @@ class Syncable extends OpStream { * to the local order (in other replicas, the order may differ). */ get version () { - return this._version; + return this.Version.toString(); } + /** @returns {Stamp} */ get Version () { - return new Stamp(this._version); + return this._rdt.Version; } get author () { @@ -140,119 +106,156 @@ class Syncable extends OpStream { /** Syncable type name * @returns {String} */ get type () { - return this.constructor.id; + return this.Type.toString(); + } + + get clazz () { + return this.constructor.RDT.Type.value; } /** @returns {Stamp} - the object's type with all the type parameters */ get Type () { - // TODO type parameters - return new Stamp(this.type, '0'); + return this._rdt.Type; } - get host () { - return this._host; - } /** Objects created by supplying an id need to query the upstream * for their state first. Until the state arrives, they are * stateless. Use `obj.once(callback)` to get notified of state arrival. */ hasState () { - return this._state !== null; - } - - get state () { - return this._state===null ? null : this._state.toString(); + return !this.Version.isZero(); } get spec () { return new Spec([ - this.type, this._id, this._version, Op.STATE + this.Type, this.Id, this.Version, Op.STATE ]); } close () { - this._emit(null); + this._rdt.off(this); // FIXME OpStream listener + this._rdt = null; } get typeid () { - if (null===this._typeid) { - this._typeid = this.TypeId.toString(Spec.ZERO); - } - return this._typeid; + return this.TypeId.toString(Spec.ZERO); } get TypeId () { - return new Spec([this.type, this._id, Stamp.ZERO, Stamp.ZERO]); + return new Spec([this.Type, this.Id, Stamp.ZERO, Stamp.ZERO]); } /** Invoke a listener after applying an op of this name * @param {String} op_name - name of the op * @param {Function} callback - listener */ - onOp (op_name, callback) { - this.on('.'+op_name, callback); - } + // onOp (op_name, callback) { + // this.on('.'+op_name, callback); + // } /** fires once the object gets some state */ - onceReady (callback) { - if (this._version!=='0') - callback(); - else - super.once(callback); + onceStateful (callback) { + if (!this.Version.isZero()) + callback(); + else + super.once(callback); } - clone () { - return new this.constructor(this._state.clone(), null); + onceSync (callback) { + super.on(function (op) { // FIXME catch reon + if (op.isOn()) { + callback (op); + return OpStream.ENOUGH; + } + return OpStream.OK; + }); } - /** Returns a subscription op for this object */ - toOnOff (is_on) { - let name = is_on ? Op.ON : Op.OFF; - let spec = new Spec([this.type, this._id, this._version, name]); - return new Op(spec, ''); + toString () { + return this.toOp().toString(); } - toOp () { - let spec = new Spec([this.type, this._id, this._version, Op.METHOD_STATE]); - return new Op(spec, this._state.toString()); + static addClass (fn) { + Syncable._classes[fn.RDT.Type.value] = fn; } - toString () { - return this._state && this._state.toString(); + /** @param {Stamp|String|Base64x64} type */ + static getClass (type) { + const Type = new Stamp(type); + return Syncable._classes[Type.value]; } -} - -Syncable._classes = Object.create(null); -Syncable._classes.Syncable = Syncable; -Syncable.id = "Syncable"; - -Syncable.multiHost = false; -Syncable.defaultHost = null; + static getRDTClass (type) { + return Syncable.getClass(type).RDT; + } -module.exports = Syncable; +} /** Abstract base class for all replicated data types; not an OpStream - * RDT is a reducer: (state_string, op) -> new_state_string + * RDT is a reducer: (state, op) -> new_state */ -class RDT { +class RDT extends OpStream { /** - * @param {String} state - the serialized state string + * @param {Op} state - the serialized state + * @param {Client} host */ - constructor (state) { + constructor (state, host) { + super(); + /** The id of an object is typically the timestamp of the first + operation. Still, it can be any Base64 string (see swarm-stamp). */ + this._id = state.spec.Id; + this._host = host; + /** Timestamp of the last change op. */ + this._version = null; + this._apply(state); + } + + offer (op) { + this._apply(op); + if (this._host) + this._host.offer(op); } - apply (op) { - switch (op.name) { - case "0": this.noop(); break; - case "on": break; - default: console.warn("unknown op", op.toString()); break; + _apply (op) { + switch (op.spec.method) { + case "0": + this.noop(); + this._version = op.spec.Stamp; + break; + case "~": + this.reset(op); + this._version = op.spec.Stamp; + break; + case "off": break; + case "on": + if (op.spec.Stamp.isZero() && !this.Version.isZero()) + this._host.offer(this.toOp()); + break; + default: + this._version = op.spec.Stamp; + break; } + this._emit(op); } noop () { } + reset (op) { + } + + get Type () { + return this.constructor.Type; + } + + get Version () { + return this._version; + } + + get Id () { + return this._id; + } + /** * @returns {String} - the serialized state string */ @@ -260,12 +263,36 @@ class RDT { return ""; } + /** Returns a subscription op for this object */ + toOnOff (is_on) { + let name = is_on ? Op.STAMP_ON : Op.STAMP_OFF; + let spec = new Spec([this.Type, this.Id, this.Version, name]); + return new Op(spec, ''); + } + + toOp () { + return new Op([ + this.Type, + this._id, + this._version, + Op.STAMP_STATE + ], this.toString()); + } + clone () { - return new this.constructor(this.toString()); + return new this.constructor(this.toOp()); } } Syncable.RDT = RDT; +Syncable.RDT.Type = new Stamp("Syncable"); // FIXME TYPE + +Syncable._classes = Object.create(null); +Syncable.defaultHost = null; + +Syncable.addClass(Syncable); + +module.exports = Syncable; // ----8<---------------------------- diff --git a/swarm-syncable/src/URL.js b/swarm-syncable/src/URL.js index a6425b4..ca5280b 100644 --- a/swarm-syncable/src/URL.js +++ b/swarm-syncable/src/URL.js @@ -1,4 +1,6 @@ "use strict"; +const swarm = require('swarm-protocol'); +const Base64x64 = swarm.Base64x64; /** node url lib 0.7KLoC and that is too much. We support a limited subset of * URL syntax using regex-based parsing */ @@ -8,27 +10,32 @@ class URL { URL.RE_URI.lastIndex = 0; const m = URL.RE_URI.exec(url); if (!m) - throw new Error('invalid URL syntax'); + throw new Error('invalid URL syntax: '+url); this.url = m[0]; this.scheme = m[1].split('+'); this.creds = m[2]; - this.replica = undefined; - this.password = undefined; - this.hostname = m[3]; - this.port = m[4] ? parseInt(m[4]) : 0; - this.host = this.hostname ? - this.hostname + (this.port?':'+this.port:'') : undefined; - this.path = m[5]; + this.replica = m[3]; + this.password = m[4]; + this.host = m[5]; + this.hostname = m[6]; + this.port = m[7] ? parseInt(m[7]) : 0; + this.path = m[8]; this.basename; - this.search = m[6]; + this.search = m[9]; this.query; - this.hash = m[7]; + this.hash = m[10]; } get protocol () { return this.scheme.join('+'); } + get basename () { + if (!this.path) return undefined; + const i = this.path.lastIndexOf('/'); + return i===-1 ? this.path : this.path.substr(i+1); + } + clone () { return new URL(this.toString()); } @@ -53,15 +60,16 @@ class URL { } URL.RE_URI = new RegExp( - "^(?:([\\w\\-]+(?:\\+[\\w\\-]+)*):)" + // scheme + ("^(?:([\\w\\-]+(?:\\+[\\w\\-]+)*):)" + // scheme "(?://" + - "(?:([^/?#\\s]*)@)?" + // credentials - "((?:[^/?#:@\\s]+\\.)*[^/?#:@\\s]+)" + // domain - "(?::([0-9]+))?" + // port + "(?:((B)(?:\\:(\\w+))?)@)?" + // credentials + "(((?:[^/?#:@\\s]+\\.)*[^/?#:@\\s]+)" + // domain + "(?::([0-9]+))?)" + // port ")" + "(/[^?#'\"\\s]*)?" + // path "(?:\\?([^'\"#\\s]*))?" + // query - "(?:#(\\S*))?$", // fragment + "(?:#(\\S*))?$") // fragment + .replace(/B/g, Base64x64.rs64x64), "gi" ); diff --git a/swarm-syncable/test/01_Syncable.js b/swarm-syncable/test/01_Syncable.js index b82fbe6..f42cb6c 100644 --- a/swarm-syncable/test/01_Syncable.js +++ b/swarm-syncable/test/01_Syncable.js @@ -1,5 +1,5 @@ "use strict"; -var tap = require('tap').test; +var tap = require('tape').test; var swarm = require('swarm-protocol'); var Spec = swarm.Spec; @@ -15,23 +15,26 @@ tap ('syncable.02.A empty cycle', function (t) { clock: new stamp.LamportClock('anon') }); host.go();*/ - var ops = Op.parseFrame( - '/Syncable#time+author!time+author.~\n' + - '/Syncable#time+author!update+author.0\n' - ); - t.equals(ops.length, 2); - - var empty = new Syncable(); + var ops = Op.parseFrame([ + '/Syncable#time+author!0.~\n', + '/Syncable#time+author!time+author.~\n', + '/Syncable#time+author!update+author.0\n', + '/Syncable#time+author!time+author.on+replica\n' + ].join('\n')); + t.equals(ops.length, 4); + + const zero = ops[0]; + const state = ops[1]; + const noop = ops[2]; + const reon = ops[3]; + + let rdt = new Syncable.RDT(zero, null); + var empty = new Syncable(rdt); t.equal(empty.version, '0', 'version comes from an op'); - t.equal(empty.id, '0', 'id comes from an op'); - - empty.noop(); - - t.equal(empty.version, '0000000001', 'detached versions'); - t.equal(empty.id, '0', 'still no id'); + t.equal(empty.id, 'time+author', 'id comes from an op'); - empty.offer(ops[0]); + rdt._apply(state); t.equal(empty.version, 'time+author', 'version id OK'); t.equal(empty.id, 'time+author', 'id OK'); @@ -39,10 +42,7 @@ tap ('syncable.02.A empty cycle', function (t) { t.equal(empty.typeid, '/Syncable#time+author'); t.ok(empty.hasState()); - let check = 0; - empty.onOp('0', ()=>check++ ); - - empty.offer(ops[1]); + rdt._apply(noop); t.equal(empty.version, 'update+author', 'version id OK'); t.ok(empty.Version.eq( new Stamp('update+author') )); @@ -50,7 +50,8 @@ tap ('syncable.02.A empty cycle', function (t) { t.equal(empty.author, 'author'); t.equal(empty.typeid, '/Syncable#time+author'); - t.equal(check, 1); + rdt._apply(reon); + t.equal(empty.version, 'update+author', 'version id OK'); t.end(); }); @@ -107,4 +108,4 @@ tap('syncable.02.D Host.get / Swarm.get', function (t) { t.end(); }); -*/ \ No newline at end of file +*/ diff --git a/swarm-syncable/test/02_Client.js b/swarm-syncable/test/02_Client.js index ed47d14..5d3baa1 100644 --- a/swarm-syncable/test/02_Client.js +++ b/swarm-syncable/test/02_Client.js @@ -1,31 +1,40 @@ "use strict"; -let tap = require('tap').test; -let swarm = require('swarm-protocol'); -let Op = swarm.Op; -let SwarmMeta = require('../src/Swarm'); -let Client = require('../src/Client'); -//let FakeClient = require('./FakeClient'); -let OpStream = require('../src/OpStream'); -let LWWObject = require('../src/LWWObject'); +const tap = require('tape').test; +const swarm = require('swarm-protocol'); +const Op = swarm.Op; +const Client = require('../src/Client'); +//const FakeClient = require('./FakeClient'); +const OpStream = require('../src/OpStream'); +const LWWObject = require('../src/LWWObject'); tap ('syncable.02.A SwarmMeta API', function (t) { - let host = new Client('/Swarm#test', new OpStream()); - let p = new SwarmMeta(host); - t.equals(p.get('Clock'), undefined); - p.set('Clock', 'Logical'); - t.equals(p.get('Clock'), 'Logical'); - p.set('ClockOffst', -12345); - t.equals(p.get('ClockOffst'), -12345); - p.set('Object', 'hi'); - t.equals(p.get('Object'), 'hi'); - t.throws(function () { - p.set('New\nLine', 'abc'); - }); - t.throws(function () { - p.set('', 'abc'); - }); + const ops = Op.parseFrame([ + '/Swarm#test!0.on', + '/Swarm#test!time.~+ReplicaSSN !1.Clock "Logical"', + '/Swarm#test!time.on+ReplicaSSN', + '' + ].join('\n')); + + let host = new Client('swarm+0://02.A/test'); + let meta = host.meta; + + t.notOk(meta.hasState()); + t.notOk(host.time()); + t.equals(meta.get('Clock'), undefined); + + const upstream = OpStream.QUEUES['02.A']; + t.equals(upstream.ops.length, 1); + t.equals(upstream.ops.shift()+'', ops[0]+''); + + upstream._emit(ops[1]); + t.equals(meta.get('Clock'), 'Logical'); + t.ok(meta.hasState()); + t.equals(host.time().toString(), 'time01+ReplicaSSN'); + upstream._emit(ops[2]); + t.equals(host.time().toString(), 'time02+ReplicaSSN'); + t.end(); }); @@ -33,44 +42,65 @@ tap ('syncable.02.A SwarmMeta API', function (t) { tap ( 'syncable.02.B Client add/removeSyncable API', function (t) { - // by default, a Client has a {map} as a backing storage and no upstream - let host = new Client('/Swarm#test!0+replica'); - // FIXME - host._clock = new swarm.Clock('replica', {Clock: 'Logical'}); + const ops = Op.parseFrame([ + '/Swarm#test!0.on+0eplica Password: 1', + '/Swarm#test!time.~+ReplicaSSN !1.Clock "Logical"', + '/Swarm#test!time.on+ReplicaSSN', + '/LWWObject#time02+ReplicaSSN!time02+ReplicaSSN.~=\n\t!time03+ReplicaSSN.key\t"value"', + '/LWWObject#time02+ReplicaSSN!time02+ReplicaSSN.on+ReplicaSSN', + '/LWWObject#time02+ReplicaSSN!time04+ReplicaSSN.key "new value"', + '' + ].join('\n')); + + let host = new Client('swarm+0://0eplica:1@02.B/test'); + const upstream = OpStream.QUEUES['02.B']; + t.equals(upstream.ops.length, 1); + t.equals(upstream.ops.shift().toString(), ops[0].toString()); + let synced = false; + host.onSync( () => synced = true ); + t.notOk(synced); + upstream._emit(ops[1]); + t.notOk(synced); + t.equals(host.time().toString(), 'time01+ReplicaSSN'); // cache init + upstream._emit(ops[2]); + t.ok(synced); // by-value constructor - let props = new LWWObject({key: "value"}); - host.addNewSyncable(props); + let props = host.newLWWObject({key: "value"}); // write stamping t.equals(props.get('key'), 'value'); let stamp = props.StampOf('key'); - t.equals(stamp.origin, '0'); + t.equals(stamp.origin, 'ReplicaSSN'); + const state = upstream.ops.shift(); + t.equals(state.spec.method, Op.METHOD_STATE); + t.equals(state.toString(), ops[3]+''); + const on = upstream.ops.shift(); + t.equals(on.spec.method, Op.METHOD_ON); + t.equals( on.spec.object, on.spec.object ); + t.equals(on.toString(), ops[4]+''); + + t.equals(props.StampOf('key').origin, 'ReplicaSSN'); props.set('key', 'new value'); t.equals(props.get('key'), 'new value'); - t.equals(props.StampOf('key').origin, 'replica'); - - // the op gets some logical timestamp not far from zero - t.ok(stamp.value < '000000000A'); - props.set('key', 'value2'); - t.equals(props.get('key'), 'value2'); - t.ok(stamp.lt(props.StampOf('key'))); - props.set('key', 'value3'); - t.equals(props.get('key'), 'value3'); + t.equals(props.StampOf('key').origin, 'ReplicaSSN'); + const op = upstream.ops.shift(); + t.equals(op.toString(), ops[5]+''); + // by-id constructor, duplicate prevention - let porps = host.getBySpec(props.spec); + let porps = host.fetch(props.spec); t.ok(porps===props); - props.close(); + /*props.close(); t.throws(function () { // the object is closed props.set('key', 'fails'); }); - let props2 = host.getBySpec(props.spec); + let props2 = host.fetch(props.spec); t.ok(props!==props2); t.ok(props2.get('key')===undefined); // NO STORAGE synchronous state load host.close(); t.throws(function () { // the host and the object are closed props2.set('key', 'fails'); - }); + });*/ t.end(); diff --git a/swarm-syncable/test/0A_URL.js b/swarm-syncable/test/0A_URL.js index d1ca31a..2923eb3 100644 --- a/swarm-syncable/test/0A_URL.js +++ b/swarm-syncable/test/0A_URL.js @@ -1,13 +1,16 @@ "use strict"; -let tape = require('tap').test; +let tape = require('tape').test; let swarm = require('swarm-protocol'); let URL = require('../src/URL'); tape ('syncable.0A.A OpStream URL - basic syntax', function (t) { - const url_str = 'ws://host.com:1234/path?query#hash'; + const url_str = 'ws://user:pwd@host.com:1234/path?query#hash'; const url1 = new URL(url_str); t.equals(url1.protocol,"ws"); + t.equals(url1.replica, 'user'); + t.equals(url1.creds, 'user:pwd'); + t.equals(url1.password, 'pwd'); t.equals(url1.host,"host.com:1234"); t.equals(url1.hostname,"host.com"); t.equals(url1.port,1234); From ca4732d2d6a79c7e43283a48361d42d3c0f76b71 Mon Sep 17 00:00:00 2001 From: Victor Grishchenko Date: Mon, 3 Oct 2016 21:14:14 +0500 Subject: [PATCH 06/51] Cache OpStream (abstract, in-memory) --- swarm-syncable/src/Cache.js | 147 ++++++++++++++++++++++++++++++++ swarm-syncable/src/Client.js | 10 ++- swarm-syncable/src/LWWObject.js | 4 + swarm-syncable/src/OpStream.js | 11 +-- swarm-syncable/src/URL.js | 6 ++ swarm-syncable/test/09_Cache.js | 87 +++++++++++++++++++ 6 files changed, 256 insertions(+), 9 deletions(-) create mode 100644 swarm-syncable/test/09_Cache.js diff --git a/swarm-syncable/src/Cache.js b/swarm-syncable/src/Cache.js index e69de29..0098637 100644 --- a/swarm-syncable/src/Cache.js +++ b/swarm-syncable/src/Cache.js @@ -0,0 +1,147 @@ +"use strict"; +const swarm = require('swarm-protocol'); +const Op = swarm.Op; +const OpStream = require('./OpStream'); +const URL = require('./URL'); + +/** Enables client reconnections and caching. + * Client emits !0 subscriptions. */ +class Cache extends OpStream { + + /** + * @param {OpStream} client - the client replica + **/ + constructor (url, options) { + super(); + this.origin = null; + this.url = new URL(url); + this.upstream = OpStream.connect(this.url.nested()); + this.upstream.on(this); + this.log = []; + this.dirty = Object.create(null); + this.__ = Object.create(null); + this.__log = []; + this._timer = null; // FIXME ensure flushes don't overlap + } + + /** this method to be overridden with some key-value storage access */ + _cache_flush ( callback ) { + // Object.keys(this.dirty).forEach( object => { FIXME back .on + // const state = this.client.get(object); + // this.__.log[object] = state.toOp(); + // } ); + this.__log = this.log; + callback && callback(); + } + + /** this method to be overridden with some key-value storage access */ + _cache_read ( key, callback ) { + const cached = this.__[key]; + if (cached) + callback(Op.parseFrame(cached+'\n')[0]); + else + callback(null); + } + + _cache_log_read (callback) { + + } + + _resubscribe (opstream) { + // convention: meta object is the first?!!! + this.client.activeObjects().forEach( obj => { + this._emit(obj.toOn()); + } ); + } + + markObjectDirty (op) { + this.dirty[op.object] = 1; + if (this._timer===null) //!!!! + this._timer = setTimeout( this._cache_flush.bind(this), 1000 ); + } + + logOp (op) { + if (this._timer===undefined) + return; + if (this._timer!==null) { + clearTimeout(this._timer); + this._timer = undefined; + } + + this._logged.push(op); + + setTimeout(()=>{ // batching guarantees + this._timer = null; + const logged = this._logged; + this._logged = []; + this._cache_flush(() => { + this._emitAll(logged); + }); + }, 0); + } + + _apply (op) { + switch (op.method) { + + case Op.METHOD_STATE: + this.markObjectDirty(op); + this._emit(op); + break; + + case Op.METHOD_ON: + case Op.METHOD_OFF: + this._emit(op); + break; + + case Op.METHOD_ERROR: + this._emit(op); + break; + + default: + if (op.origin===this.origin) { + while (this.log.length && this.log[0].stamp<=op.stamp) + this.log.shift(); + } + this.markObjectDirty(op); + this._emit(op); + break; + + } + } + + offer (op) { + switch (op.spec.method) { + + case Op.METHOD_STATE: + this.markDirty(op.object); + this.upstream.offer(op); + break; + + case Op.METHOD_ON: + case Op.METHOD_OFF: + this._cache_read(op.spec.object, state => { + if (state) { + this._emit(state); + op = op.restamped(state.stamp); + } + this.upstream.offer(op); + }); + break; + + case Op.METHOD_ERROR: + this.upstream.offer(op); + break; + + default: + this.log.push(op); + this._cache_flush(()=>this.upstream.offer(op)); + break; + + } + } + +} + +OpStream._URL_HANDLERS['mem'] = Cache; + +module.exports = Cache; \ No newline at end of file diff --git a/swarm-syncable/src/Client.js b/swarm-syncable/src/Client.js index eba7a24..ee85cf9 100644 --- a/swarm-syncable/src/Client.js +++ b/swarm-syncable/src/Client.js @@ -51,7 +51,7 @@ class Client extends OpStream { this._meta = this.get( SwarmMeta.RDT.Type, this.dbid, - state => { + state => { // FIXME htis must be state!!! this._clock = new swarm.Clock(state.scope, this._meta.filterByPrefix('Clock')); this._id = state.scope; this._clock.seeTimestamp(state.spec.Stamp); @@ -73,12 +73,12 @@ class Client extends OpStream { } onceReady (callback) { - this._meta.onceReady(callback); + this._meta.onceStateful(callback); } /** Inject an op. */ _apply (op) { - const rdt = this._syncables[op.typeid]._rdt; + const rdt = this._syncables[op.spec.object]._rdt; if (!op.spec.Stamp.isAbnormal() && this._clock) this._clock.seeTimestamp(op.spec.Stamp); if (op.isOnOff()) @@ -166,9 +166,11 @@ class Client extends OpStream { const on = rdt.toOnOff(true).scoped(this._id); if (on.spec.clazz==='Swarm' && this._url.password) on._value = 'Password: '+this._url.password; // FIXME E E + const syncable = new fn(rdt, on_state); + this._syncables[spec.object] = syncable; this._upstream.offer(on, this); this._unsynced.set(spec.object, 1); - return this._syncables[spec.object] = new fn(rdt, on_state); + return syncable; } _remove_syncable (obj) { diff --git a/swarm-syncable/src/LWWObject.js b/swarm-syncable/src/LWWObject.js index 65aeac3..9c30ffd 100644 --- a/swarm-syncable/src/LWWObject.js +++ b/swarm-syncable/src/LWWObject.js @@ -59,6 +59,10 @@ class LWWObject extends Syncable { return this._values; } + save () { + // TODO + } + _rebuild (op) { const name = op ? op.spec.method : Op.METHOD_STATE; // :( if (name===Op.METHOD_STATE) { // rebuild diff --git a/swarm-syncable/src/OpStream.js b/swarm-syncable/src/OpStream.js index f142b36..548a1c6 100644 --- a/swarm-syncable/src/OpStream.js +++ b/swarm-syncable/src/OpStream.js @@ -202,11 +202,12 @@ class OpStream { */ static connect (url, options) { - const m = /^([\w\-]+)(\+[\w\-]+)*:/.exec(url); - if (!m) throw new Error("invalid url"); - const top_proto = m[1]; + if (url.constructor!==URL) + url = new URL(url.toString()); + const top_proto = url.scheme[0]; const fn = OpStream._URL_HANDLERS[top_proto]; - if (!fn) throw new Error('unknown protocol: '+top_proto); + if (!fn) + throw new Error('unknown protocol: '+top_proto); return new fn(url, options); } @@ -251,7 +252,7 @@ class CallbackOpStream extends OpStream { } _apply (op) { - return this._callback(op)===OpStream.ENOUGH || this._once ? + return (this._callback(op)===OpStream.ENOUGH || this._once) ? OpStream.ENOUGH : OpStream.OK; } diff --git a/swarm-syncable/src/URL.js b/swarm-syncable/src/URL.js index ca5280b..8bc772a 100644 --- a/swarm-syncable/src/URL.js +++ b/swarm-syncable/src/URL.js @@ -40,6 +40,12 @@ class URL { return new URL(this.toString()); } + nested () { + let next = this.clone(); + next.scheme.shift(); + return next; + } + eq (url) { return this.toString() === url.toString(); } diff --git a/swarm-syncable/test/09_Cache.js b/swarm-syncable/test/09_Cache.js new file mode 100644 index 0000000..d32e1ac --- /dev/null +++ b/swarm-syncable/test/09_Cache.js @@ -0,0 +1,87 @@ +"use strict"; +const tape = require('tape').test; +const swarm = require('swarm-protocol'); +const Cache = require('../src/Cache'); +const Client = require('../src/Client'); +const Op = swarm.Op; +const OpStream = require('../src/OpStream'); + +tape ('syncable.09.A client cache - basic', function (t) { + + const ops = Op.parseFrame([ + "/Swarm#test!0.on+0eplica", + '/Swarm#test!time01.~+ReplicaSSN=\n\t!1.Clock "Logical"\n\t!2.ClockLen 6', + '/Swarm#test!time01.on+ReplicaSSN', + '/LWWObject#id!time+origin.~ !2.cachedkey "cached_value"', + '/LWWObject#id!time02+origin.new_key+ReplicaSSN "new_value"', + ".on+ReplicaSSN", + '/LWWObject#id!time03+ReplicaSSN.changedkey "changed_value"', + "" + ].join('\n')); + const hs_op = ops[0]; + const meta_op = ops[1]; + const re_hs_op = ops[2]; + const cached_state_op = ops[3]; + const new_op = ops[4]; + const re_on_op = ops[5]; + const change_op = ops[6]; + + const client = new Client('swarm+mem+0://0eplica@09.A/test'); + const cache = client._upstream; + const up = OpStream.QUEUES['09.A']; + + cache.origin = 'ReplicaSSN'; // FIXME recognize hs + + let synced = false; + client.onceReady( () => synced = true ); + + // at this point, the cache has the outgoing handshake + t.equal(synced, false); + const hs = up.ops.shift(); + t.equals(hs.toString(), hs_op.toString()); + up._emit(meta_op); + up._emit(re_hs_op); + t.equal(synced, true); + t.equal( client.time().origin, 'ReplicaSSN' ); + + // inject a cache record + const oid = new_op.spec.object; + cache.__[oid] = cached_state_op.toString(); + + // open an object + let obj_stateful = false; + let obj_synced = false; + const obj = client.get( new_op.spec.type, new_op.spec.id, () => obj_stateful = true ); + t.ok(obj_stateful); + t.ok(obj.hasState()); + t.equals(obj.get('cachedkey'), "cached_value"); + + // check the subscription + const on = up.ops.shift(); + t.equals(on.spec.object, oid); + up._emit(new_op); + // TODO onSync t.notOk(obj_synced); + t.equals(obj.version, new_op.spec.stamp); + t.equals(obj.get('new_key'), "new_value"); + up._emit(re_on_op); + // TODO t.ok(obj_synced); + + // create new op, log, ack + // obj.changed_key = "changed_value"; + // obj.save(); + obj.set('changedkey', "changed_value"); + const change = up.ops.shift(); + t.equals(change.toString(), change_op.toString()); + t.equals(cache.__log.length, 1); + t.equals(cache.__log[0].toString(), change_op.toString()); + up._emit(change_op); + t.equals(cache.__log.length, 0); + const cached = cache.__[oid]; // FIXME back .on + //t.equals(cached.spec.stamp, change.spec.stamp); + //t.ok(cached.value.indexOf("changedkey")!==-1); + + // create object, check cache + + t.end(); + +}); \ No newline at end of file From 9bc44d4c836055a94f6c62196781e4ba6c370bfa Mon Sep 17 00:00:00 2001 From: Victor Grishchenko Date: Sun, 9 Oct 2016 22:35:19 +0500 Subject: [PATCH 07/51] refac: make Op extend Spec --- swarm-protocol/package.json | 6 +- swarm-protocol/src/Op.js | 136 ++++++++++----------------- swarm-protocol/src/Spec.js | 23 ++--- swarm-protocol/src/Stamp.js | 8 ++ swarm-protocol/test/00_base64.js | 2 +- swarm-protocol/test/01_Stamp.js | 2 +- swarm-protocol/test/02_Clock.js | 2 +- swarm-protocol/test/03_Spec.js | 8 +- swarm-protocol/test/04_Op.js | 2 +- swarm-protocol/test/05_VV.js | 2 +- swarm-protocol/test/06_replica_id.js | 4 +- 11 files changed, 81 insertions(+), 114 deletions(-) diff --git a/swarm-protocol/package.json b/swarm-protocol/package.json index e91cdb0..9e5f1e9 100644 --- a/swarm-protocol/package.json +++ b/swarm-protocol/package.json @@ -1,6 +1,6 @@ { "name": "swarm-protocol", - "version": "1.2.1", + "version": "1.2.3", "homepage": "http://github.com/gritzko/swarm", "repository": { "type": "git", @@ -23,6 +23,10 @@ "browser": "index.js", "dependencies": { }, + "devDependencies": { + "tape": "4.6.2", + "tap-diff": "^0.1.1" + }, "scripts": { "test": "make test" } diff --git a/swarm-protocol/src/Op.js b/swarm-protocol/src/Op.js index 8bb7dac..ff77286 100644 --- a/swarm-protocol/src/Op.js +++ b/swarm-protocol/src/Op.js @@ -7,57 +7,28 @@ var Spec = require('./Spec'); * Immutable Swarm op, see the specification at * https://gritzko.gitbooks.io/swarm-the-protocol/content/op.html * */ -class Op { - - constructor (spec, value, source) { - this._spec = this._value = null; - if (spec===undefined) { - return Op.NON_SPECIFIC_NOOP; - } else if (spec.constructor===Op) { - this._spec = spec._spec; - this._value = spec._value; - //this._source = spec._source; - } else if (spec.constructor===Spec) { - this._spec = spec; - this._value = value.toString(); - //this._source = source || Stamp.ZERO; - } else { - this._spec = new Spec(spec); - this._value = value.toString(); - } +class Op extends Spec { + + constructor (spec, value) { + super(spec); + this._value = value || ''; } get spec () { - return this._spec; + return new Spec (this); } get specifier () { - return this._spec; + return this.spec; } get value () { return this._value; } - /** an immediate connection id this op was received from, where - * op.source.value is a connection id per se, while - * op.source.origin is the id of the connected replica - * (op.spec.origin is the id of a replica that created the op) */ - get source () { - return this._source; - } - - get origin () { - return this._spec.origin; - } - - get scope () { - return this._spec.scope; - } - toString (defaults) { - let ret = this._spec.toString(defaults); - if (!this._value) { + let ret = super.toString(defaults); + if (this._value==='') { } else if (this._value.indexOf('\n')===-1) { ret += '\t' + this._value; } else { @@ -68,7 +39,7 @@ class Op { /** whether this is not a state-mutating op */ isPseudo () { - return Op.PSEUDO_OP_NAMES.indexOf(this._spec.name.value)!==-1; + return Op.PSEUDO_OP_NAMES.indexOf(this.method)!==-1; } /** parse a frame of several serialized concatenated newline- @@ -122,90 +93,81 @@ class Op { return frame; } - get type () { return this._spec.type; } - get id () { return this._spec.id; } - get stamp () { return this._spec.stamp; } - get name () { return this._spec.name; } - get typeid () { return this._spec.typeid; } - - isOn () { return this.spec.method === Op.METHOD_ON; } + isOn () { return this.method === Op.METHOD_ON; } - isOff () { return this.spec.method === Op.METHOD_OFF; } + isOff () { return this.method === Op.METHOD_OFF; } isOnOff () { return this.isOn() || this.isOff(); } - isMutation () { + isHandshake () { + return this.isOnOff() && this.clazz==='Swarm'; + } + + isMutation () { // FIXME abnormal vs normal return !this.isOnOff() && !this.isError() && !this.isState(); } isState () { - return this.spec.method === Op.METHOD_STATE; + return this.method === Op.METHOD_STATE; } isNoop () { - return this.spec.method === Op.METHOD_NOOP; + return this.method === Op.METHOD_NOOP; } isError () { - return this.spec.method === Op.METHOD_ERROR; + return this.method === Op.METHOD_ERROR; } isNormal () { - return !this._spec.Name.isAbnormal() && !this.isOnOff(); // TODO ~on? - } - - isSameObject (spec) { - if (spec.constructor===Op) { - spec = spec.spec; - } else if (spec.constructor!==Spec) { - spec = new Spec(spec); - } - return this.spec.isSameObject(spec); + return !this.Name.isAbnormal() && !this.isOnOff(); // TODO ~on? } /** * @param {String} message + * @param {String|Base64x64} scope - the receiver * @returns {Op} error op */ error (message, scope) { - let spec = this.spec.rename(Stamp.ERROR); - if (scope) - spec = spec.rescope(scope); - return new Op(spec, message); + const Name = new Stamp(Base64x64.INCORRECT, scope || '0'); + return new Op([this.Type, this.Id, this.Stamp, Name], message); } - /** @param {Base64x64|String} stamp */ - overstamped (stamp) { - if (this.spec.isScoped()) + /** @param {Base64x64|String} new_stamp */ + overstamped (new_stamp) { + if (this.isScoped()) throw new Error('can not overstamp a scoped op'); - let spec = new Spec([ - this.spec.Type, - this.spec.Id, - new Stamp(stamp, this.spec.origin), - new Stamp(this.spec.method, this.spec.time) - ]); - return new Op(spec, this.value); + return new Op([ + this.Type, + this.Id, + new Stamp(new_stamp, this.origin), + new Stamp(this.method, this.time) + ], this._value); } clearstamped (new_scope) { - if (!this.spec.isScoped()) - return !new_scope ? this : this.scoped(new_scope); - let spec = new Spec([ - this.spec.Type, - this.spec.Id, - new Stamp(this.spec.scope, this.spec.origin), - new Stamp(this.spec.method, new_scope||'0') - ]); - return new Op(spec, this.value); + if (!this.isScoped() && !new_scope) + return this; + return new Op ([ + this.Type, + this.Id, + new Stamp(this.scope ? this.scope : this.time, this.origin), + new Stamp(this.method, new_scope||'0') + ], this._value); } - restamped (stamp) { - return new Op(this._spec.restamp(stamp), this._value); + stamped (stamp) { + return new Op([this.Type, this.Id, stamp, this.Name], this._value); } scoped (scope) { - return new Op(this._spec.scoped(scope), this._value); + return new Op([ + this.Type, + this.Id, + this.Stamp, + new Stamp(this.method, scope) + ], this._value); } } diff --git a/swarm-protocol/src/Spec.js b/swarm-protocol/src/Spec.js index 40b4d74..85f5578 100644 --- a/swarm-protocol/src/Spec.js +++ b/swarm-protocol/src/Spec.js @@ -46,7 +46,7 @@ class Spec { let t = this._toks = [Stamp.ZERO, Stamp.ZERO, Stamp.ZERO, Stamp.ZERO]; if (!spec) { 'nothing'; - } else if (spec.constructor===Spec) { + } else if (spec._toks && spec._toks.constructor===Array) { this._toks = spec._toks; } else if (spec.constructor===Array && spec.length===4) { for (let i = 0; i < 4; i++) { @@ -225,30 +225,23 @@ class Spec { return this._toks.every(t => t.isEmpty()); } - restamp (stamp, origin) { - if (origin) stamp = new Stamp(stamp, origin); + restamped (stamp, origin) { + if (origin) + stamp = new Stamp(stamp, origin); return new Spec([this.Type, this.Id, stamp, this.Name]); } - rename (stamp, origin) { - if (origin) stamp = new Stamp(stamp, origin); + renamed (stamp, origin) { + if (origin) + stamp = new Stamp(stamp, origin); return new Spec([this.Type, this.Id, this.Stamp, stamp]); } - /** @param {String|Base64x64} method */ - remethod (method) { - return new Spec([this.Type, this.Id, this.Stamp, new Stamp(method, this.scope)]); - } - /** @param {String|Base64x64} scope */ - scoped (scope) { + rescoped (scope) { return new Spec([this.Type, this.Id, this.Stamp, new Stamp(this.method, scope)]); } - rescope (scope) { - return this.scoped(scope); // FIXME naming conventions!!! - } - } Spec.quants = ['/', '#', '!', '.']; diff --git a/swarm-protocol/src/Stamp.js b/swarm-protocol/src/Stamp.js index 9f0d93d..af7ca70 100644 --- a/swarm-protocol/src/Stamp.js +++ b/swarm-protocol/src/Stamp.js @@ -138,6 +138,14 @@ class Stamp { (this._value===s._value && this._origin Date: Sun, 9 Oct 2016 22:55:45 +0500 Subject: [PATCH 08/51] swarm-syncable to drop .spec. --- swarm-syncable/package.json | 6 ++++-- swarm-syncable/src/Cache.js | 4 ++-- swarm-syncable/src/Client.js | 18 +++++++++--------- swarm-syncable/src/LWWObject.js | 18 +++++++++--------- swarm-syncable/src/OpStream.js | 19 ++++++++++--------- swarm-syncable/src/Syncable.js | 21 +++++++++------------ swarm-syncable/test/09_Cache.js | 10 +++++----- 7 files changed, 48 insertions(+), 48 deletions(-) diff --git a/swarm-syncable/package.json b/swarm-syncable/package.json index f81b958..75fb744 100644 --- a/swarm-syncable/package.json +++ b/swarm-syncable/package.json @@ -1,6 +1,6 @@ { "name": "swarm-syncable", - "version": "1.2.1", + "version": "1.2.4", "homepage": "http://github.com/gritzko/swarm", "repository": { "type": "git", @@ -25,7 +25,9 @@ "swarm-protocol": "^1.2.0" }, "devDependencies": { - "swarm-bat": "1.0.x" + "swarm-bat": "1.0.x", + "tape": "4.6.2", + "tap-diff": "^0.1.1" }, "scripts": { "test": "make test" diff --git a/swarm-syncable/src/Cache.js b/swarm-syncable/src/Cache.js index 0098637..c73d0cc 100644 --- a/swarm-syncable/src/Cache.js +++ b/swarm-syncable/src/Cache.js @@ -110,7 +110,7 @@ class Cache extends OpStream { } offer (op) { - switch (op.spec.method) { + switch (op.method) { case Op.METHOD_STATE: this.markDirty(op.object); @@ -119,7 +119,7 @@ class Cache extends OpStream { case Op.METHOD_ON: case Op.METHOD_OFF: - this._cache_read(op.spec.object, state => { + this._cache_read(op.object, state => { if (state) { this._emit(state); op = op.restamped(state.stamp); diff --git a/swarm-syncable/src/Client.js b/swarm-syncable/src/Client.js index ee85cf9..a24e6f3 100644 --- a/swarm-syncable/src/Client.js +++ b/swarm-syncable/src/Client.js @@ -54,11 +54,11 @@ class Client extends OpStream { state => { // FIXME htis must be state!!! this._clock = new swarm.Clock(state.scope, this._meta.filterByPrefix('Clock')); this._id = state.scope; - this._clock.seeTimestamp(state.spec.Stamp); + this._clock.seeTimestamp(state.Stamp); } ); this._meta.onceSync ( - reon => this._clock.seeTimestamp(reon.spec.Stamp) + reon => this._clock.seeTimestamp(reon.Stamp) ); if (!Syncable.defaultHost) // TODO deprecate Syncable.defaultHost = this; @@ -78,16 +78,16 @@ class Client extends OpStream { /** Inject an op. */ _apply (op) { - const rdt = this._syncables[op.spec.object]._rdt; - if (!op.spec.Stamp.isAbnormal() && this._clock) - this._clock.seeTimestamp(op.spec.Stamp); + const rdt = this._syncables[op.object]._rdt; + if (!op.Stamp.isAbnormal() && this._clock) + this._clock.seeTimestamp(op.Stamp); if (op.isOnOff()) - this._unsynced.delete(op.spec.object); + this._unsynced.delete(op.object); if (op.origin === this.origin) { - this._last_acked = op.spec.Stamp; + this._last_acked = op.Stamp; } else { if (!rdt && op.name !== "off") - this._upstream.offer(new Op(op.spec.rename('off'), '')); + this._upstream.offer(new Op(op.rename('off'), '')); else rdt._apply(op); this._emit(op); @@ -164,7 +164,7 @@ class Client extends OpStream { throw new Error('unknown syncable type '+spec); const rdt = new fn.RDT(state0, this); const on = rdt.toOnOff(true).scoped(this._id); - if (on.spec.clazz==='Swarm' && this._url.password) + if (on.clazz==='Swarm' && this._url.password) on._value = 'Password: '+this._url.password; // FIXME E E const syncable = new fn(rdt, on_state); this._syncables[spec.object] = syncable; diff --git a/swarm-syncable/src/LWWObject.js b/swarm-syncable/src/LWWObject.js index 9c30ffd..8e38ecd 100644 --- a/swarm-syncable/src/LWWObject.js +++ b/swarm-syncable/src/LWWObject.js @@ -48,7 +48,7 @@ class LWWObject extends Syncable { StampOf (name) { const at = this._rdt.at(name); - return at===-1 ? Stamp.ZERO : this._rdt.ops[at].spec.Stamp; + return at===-1 ? Stamp.ZERO : this._rdt.ops[at].Stamp; } stampOf (key) { @@ -64,13 +64,13 @@ class LWWObject extends Syncable { } _rebuild (op) { - const name = op ? op.spec.method : Op.METHOD_STATE; // :( + const name = op ? op.method : Op.METHOD_STATE; // :( if (name===Op.METHOD_STATE) { // rebuild this._values = Object.create(null); this._rdt.ops.forEach(e=>{ - this._values[e.spec.method] = JSON.parse(e.value); + this._values[e.method] = JSON.parse(e.value); }); - } else if (this._version < op.spec.stamp) { + } else if (this._version < op.stamp) { this._values[name] = JSON.parse(op.value); } else { // reorder const value = this._rdt.get(name); @@ -100,13 +100,13 @@ class LWWObjectRDT extends Syncable.RDT { at (name) { for(let i=0; i0) { - l = l.filter( x => x!==null ); - this._lstn = l.length ? l : null; - } + if (ejects.length) + ejects.forEach( e => this.off(e) ); break; case PENDING: this._lstn.push(op); @@ -143,6 +141,9 @@ class OpStream { ops.forEach(op => this._emit(op)); } + _apply (op, upstream) { + } + pollAll () { let ret = null; if (this._lstn_state()===PENDING) { @@ -160,7 +161,7 @@ class OpStream { } /** by default, an echo stream */ - offer (op) { + offer (op, downstream) { if (this._debug) console.warn('}'+this._debug+'\t'+op.toString()); this._emit(op); diff --git a/swarm-syncable/src/Syncable.js b/swarm-syncable/src/Syncable.js index d5eab5f..780c184 100644 --- a/swarm-syncable/src/Syncable.js +++ b/swarm-syncable/src/Syncable.js @@ -21,9 +21,7 @@ class Syncable extends OpStream { /** * @constructor - * @param {Op} state_op - the state to init a new object with - * (can be the type's default state, version 0) - * @param {Client} host - the client that hosts this syncable (optional) + * @param {RDT} rdt - the state to init a new object with * @param {Function} callback - callback to invoke once the object is stateful */ constructor (rdt, callback) { @@ -35,7 +33,7 @@ class Syncable extends OpStream { this._rebuild(); if (callback) - this.once(callback); + this.once(callback); // FIXME } @@ -53,8 +51,7 @@ class Syncable extends OpStream { * @param {String} op_value - the op value */ _offer (op_name, op_value) { // FIXME BAD!!! const stamp = this._rdt._host.time(); - const spec = new Spec([this.Type, this.Id, stamp, new Stamp(op_name)]); - const op = new Op(spec, op_value); + const op = new Op([this.Type, this.Id, stamp, new Stamp(op_name)], op_value); this._rdt.offer(op); } @@ -203,7 +200,7 @@ class RDT extends OpStream { super(); /** The id of an object is typically the timestamp of the first operation. Still, it can be any Base64 string (see swarm-stamp). */ - this._id = state.spec.Id; + this._id = state.Id; this._host = host; /** Timestamp of the last change op. */ this._version = null; @@ -217,22 +214,22 @@ class RDT extends OpStream { } _apply (op) { - switch (op.spec.method) { + switch (op.method) { case "0": this.noop(); - this._version = op.spec.Stamp; + this._version = op.Stamp; break; case "~": this.reset(op); - this._version = op.spec.Stamp; + this._version = op.Stamp; break; case "off": break; case "on": - if (op.spec.Stamp.isZero() && !this.Version.isZero()) + if (op.Stamp.isZero() && !this.Version.isZero()) this._host.offer(this.toOp()); break; default: - this._version = op.spec.Stamp; + this._version = op.Stamp; break; } this._emit(op); diff --git a/swarm-syncable/test/09_Cache.js b/swarm-syncable/test/09_Cache.js index d32e1ac..0cb6924 100644 --- a/swarm-syncable/test/09_Cache.js +++ b/swarm-syncable/test/09_Cache.js @@ -45,23 +45,23 @@ tape ('syncable.09.A client cache - basic', function (t) { t.equal( client.time().origin, 'ReplicaSSN' ); // inject a cache record - const oid = new_op.spec.object; + const oid = new_op.object; cache.__[oid] = cached_state_op.toString(); // open an object let obj_stateful = false; let obj_synced = false; - const obj = client.get( new_op.spec.type, new_op.spec.id, () => obj_stateful = true ); + const obj = client.get( new_op.type, new_op.id, () => obj_stateful = true ); t.ok(obj_stateful); t.ok(obj.hasState()); t.equals(obj.get('cachedkey'), "cached_value"); // check the subscription const on = up.ops.shift(); - t.equals(on.spec.object, oid); + t.equals(on.object, oid); up._emit(new_op); // TODO onSync t.notOk(obj_synced); - t.equals(obj.version, new_op.spec.stamp); + t.equals(obj.version, new_op.stamp); t.equals(obj.get('new_key'), "new_value"); up._emit(re_on_op); // TODO t.ok(obj_synced); @@ -77,7 +77,7 @@ tape ('syncable.09.A client cache - basic', function (t) { up._emit(change_op); t.equals(cache.__log.length, 0); const cached = cache.__[oid]; // FIXME back .on - //t.equals(cached.spec.stamp, change.spec.stamp); + //t.equals(cached.stamp, change.stamp); //t.ok(cached.value.indexOf("changedkey")!==-1); // create object, check cache From 038f1a5f53dfb0d99e7a74fc53d5456881a7b9c4 Mon Sep 17 00:00:00 2001 From: Victor Grishchenko Date: Mon, 10 Oct 2016 10:57:35 +0500 Subject: [PATCH 09/51] Syncable.RDT.Type -> Syncable.RDT.Class --- swarm-peer/src/LevelOp.js | 8 +++++--- swarm-syncable/src/Client.js | 4 ++-- swarm-syncable/src/LWWObject.js | 2 +- swarm-syncable/src/Swarm.js | 12 +++++++++++- swarm-syncable/src/Syncable.js | 15 +++++++-------- 5 files changed, 26 insertions(+), 15 deletions(-) diff --git a/swarm-peer/src/LevelOp.js b/swarm-peer/src/LevelOp.js index 1eca9d0..fa88c10 100644 --- a/swarm-peer/src/LevelOp.js +++ b/swarm-peer/src/LevelOp.js @@ -22,6 +22,7 @@ class LevelOp { * @param {Function} on_op - a callback for every op found * @param {Function} on_end - a final callback * @param {Object} options (skipOp, reverse) + * @returns {Iterator} */ scan ( from, till, on_op, on_end, options ) { options = options || Object.create(null); @@ -29,7 +30,7 @@ class LevelOp { const filter = options.filter || null; let limit = options.limit || (1<<30); if (till===null) { - till = from.restamp(swarm.Stamp.ERROR); + till = from.restamped(swarm.Stamp.ERROR); } let i = this._db.iterator({ gte: from.toString(), @@ -57,6 +58,7 @@ class LevelOp { } }; i.next(levelop_read_op); + return i; } /** @param {Array} ops - an array of Op to save @@ -106,13 +108,13 @@ LevelOp.Put = class LevelOpPut { this.key = op.spec.toString(); this.value = op.value; } -} +}; LevelOp.Del = class LevelOpDel { constructor(spec) { this.type = 'del'; this.key = spec.toString(); } -} +}; module.exports = LevelOp; \ No newline at end of file diff --git a/swarm-syncable/src/Client.js b/swarm-syncable/src/Client.js index a24e6f3..ff30c5f 100644 --- a/swarm-syncable/src/Client.js +++ b/swarm-syncable/src/Client.js @@ -49,7 +49,7 @@ class Client extends OpStream { this._upstream.on(this); this._unsynced = new Map(); this._meta = this.get( - SwarmMeta.RDT.Type, + SwarmMeta.RDT.Class, this.dbid, state => { // FIXME htis must be state!!! this._clock = new swarm.Clock(state.scope, this._meta.filterByPrefix('Clock')); @@ -260,7 +260,7 @@ class Client extends OpStream { } newLWWObject (init_obj) { - return this.create(LWWObject.RDT.Type, init_obj); + return this.create(LWWObject.RDT.Class, init_obj); } } diff --git a/swarm-syncable/src/LWWObject.js b/swarm-syncable/src/LWWObject.js index 8e38ecd..88ebd42 100644 --- a/swarm-syncable/src/LWWObject.js +++ b/swarm-syncable/src/LWWObject.js @@ -142,5 +142,5 @@ class LWWObjectRDT extends Syncable.RDT { } LWWObject.RDT = LWWObjectRDT; -LWWObjectRDT.Type = new Stamp('LWWObject'); +LWWObjectRDT.Class = 'LWWObject'; Syncable.addClass(LWWObject); diff --git a/swarm-syncable/src/Swarm.js b/swarm-syncable/src/Swarm.js index cbc1e2f..5631b0b 100644 --- a/swarm-syncable/src/Swarm.js +++ b/swarm-syncable/src/Swarm.js @@ -7,6 +7,10 @@ const ReplicaIdScheme = swarm.ReplicaIdScheme; /** Database metadata object. */ class Swarm extends LWWObject { + constructor (rdt, host) { + super(rdt, host); + this._scheme = null; + } filterByPrefix (prefix) { let ret = Object.create(null); @@ -18,6 +22,12 @@ class Swarm extends LWWObject { return ret; } + get replicaIdScheme () { + if (this._scheme===null) + this._scheme = new ReplicaIdScheme(this.get('DBIdScheme')); + return this._scheme; + } + } class SwarmRDT extends LWWObject.RDT { @@ -27,7 +37,7 @@ class SwarmRDT extends LWWObject.RDT { } Swarm.RDT = SwarmRDT; -SwarmRDT.Type = new swarm.Stamp('Swarm'); // FIXME rename to CLASS +SwarmRDT.Class = 'Swarm'; // FIXME rename to CLASS Syncable.addClass(Swarm); module.exports = Swarm; \ No newline at end of file diff --git a/swarm-syncable/src/Syncable.js b/swarm-syncable/src/Syncable.js index 780c184..a84862b 100644 --- a/swarm-syncable/src/Syncable.js +++ b/swarm-syncable/src/Syncable.js @@ -107,7 +107,7 @@ class Syncable extends OpStream { } get clazz () { - return this.constructor.RDT.Type.value; + return this.constructor.RDT.Class; } /** @returns {Stamp} - the object's type with all the type parameters */ @@ -172,13 +172,12 @@ class Syncable extends OpStream { } static addClass (fn) { - Syncable._classes[fn.RDT.Type.value] = fn; + Syncable._classes[fn.RDT.Class] = fn; } - /** @param {Stamp|String|Base64x64} type */ - static getClass (type) { - const Type = new Stamp(type); - return Syncable._classes[Type.value]; + /** @param {String|Base64x64} type */ + static getClass (clazz) { + return Syncable._classes[clazz]; } static getRDTClass (type) { @@ -242,7 +241,7 @@ class RDT extends OpStream { } get Type () { - return this.constructor.Type; + return this.constructor.Class; } get Version () { @@ -282,7 +281,7 @@ class RDT extends OpStream { } Syncable.RDT = RDT; -Syncable.RDT.Type = new Stamp("Syncable"); // FIXME TYPE +RDT.Class = "Syncable"; Syncable._classes = Object.create(null); Syncable.defaultHost = null; From c46efda7002f4bbe82a16361ffe274b46692f8b9 Mon Sep 17 00:00:00 2001 From: Victor Grishchenko Date: Tue, 11 Oct 2016 02:39:14 +0500 Subject: [PATCH 10/51] generalize SwitchOS, merged AuthOS in, kill BatchOS --- swarm-peer/src/BatchedOpStream.js | 111 ---------- swarm-peer/src/SwitchOpStream.js | 325 ++++++++++++++++++---------- swarm-peer/test/01_switch.js | 91 ++++---- swarm-protocol/src/Base64x64.js | 18 +- swarm-syncable/src/Client.js | 43 ++-- swarm-syncable/src/OpStream.js | 23 +- swarm-syncable/src/URL.js | 1 - swarm-syncable/test/03_LWWObject.js | 29 ++- 8 files changed, 339 insertions(+), 302 deletions(-) delete mode 100644 swarm-peer/src/BatchedOpStream.js diff --git a/swarm-peer/src/BatchedOpStream.js b/swarm-peer/src/BatchedOpStream.js deleted file mode 100644 index b4822d3..0000000 --- a/swarm-peer/src/BatchedOpStream.js +++ /dev/null @@ -1,111 +0,0 @@ -"use strict"; -const swarm = require('swarm-protocol'); -const sync = require('swarm-syncable'); -const OpStream = sync.OpStream; - -/** - * BatchOpStream extends OpStream by adding some server-side processing features: - * 1. batching: ops that are offered synchronously are considered one batch; - * any result of their processing is also emitted synchronously - * 2. isolation: BOS acts as a call stack root; that avoids the problem of - * unpredictable stack traces that start in the network or in the db, then - * ricochet all around the place - * 3. backpressure: once BOS receives OpStream.SLOW_DOWN, it increases pauses - * between batches, relays SLOW_DOWN further TODO - * - * BOP is not a node.js stream. - * */ -class BatchedOpStream extends OpStream { - - constructor () { - super(); - this._ingress_batch = null; - this._processed_batch = null; - this._egress_batch = null; - this._process_next_batch_cb = this._process_next_batch.bind(this); - this._done_cb = this._done.bind(this); - this._process_cb = this._process.bind(this, this._done_cb); - } - - /** @override */ - offer (op) { - - if (this._debug) - console.warn('}'+this._debug+'\t'+op.toString()); - - if (this._ingress_batch===null) { - if (this._processed_batch===null) - process.nextTick(()=>this._process_next_batch()); - this._ingress_batch = []; - } - - this._ingress_batch.push(op); - - } - - _batch (op) { - this._egress_batch.push(op); - } - - /** totally synchronous */ - _forward_batch (ops) { - if (this._debug) - console.warn('{'+this._debug+'\t['+ops.length+']'); - super._emitAll(ops); // emit the batch synchronously - } - - _process_next_batch () { - if (this._processed_batch) { - if (this._processed_batch.length) - throw new Error('state machine XXXXX'); - if (this._egress_batch.length) - this._forward_batch(this._egress_batch); - this._processed_batch = null; - this._egress_batch = null; - } - if (this._ingress_batch) { - this._processed_batch = this._ingress_batch.reverse(); - this._ingress_batch = null; - this._egress_batch = []; - this._process_cb(); - } - } - - _process (done) { - this._process_op(this._processed_batch.pop(), done); // breaks the batch - } - - _done (err) { - if (err) { - console.error(err); - this._stop(); - } else if (this._processed_batch===null) { - console.warn(new Error('invalid callback').stack); - } else if (this._processed_batch.length) { - // TODO e.g. stack depth 100 - process.nextTick(this._process_cb); - } else { - this._process_next_batch(); - /*process.nextTick(()=>{ - if (this._processed_batch.length) - this._process_cb(); - else - this._process_next_batch(); - });*/ - } - } - - /** The method to be overloaded by implementations. */ - _process_op (op, done) { - this._batch(op); // by default, emit the same batch - done(); - } - - _end () { - this._ingress_batch = null; - super._end(); - } - -} - -module.exports = BatchedOpStream; diff --git a/swarm-peer/src/SwitchOpStream.js b/swarm-peer/src/SwitchOpStream.js index b2b4036..0a26dee 100644 --- a/swarm-peer/src/SwitchOpStream.js +++ b/swarm-peer/src/SwitchOpStream.js @@ -1,165 +1,256 @@ "use strict"; const swarm = require('swarm-protocol'); const sync = require('swarm-syncable'); -const LevelOp = require('./LevelOp'); -const BatchedOpStream = require('./BatchedOpStream'); const Spec = swarm.Spec; const Stamp = swarm.Stamp; +const Op = swarm.Op; +const OpStream = sync.OpStream; +const Base64x64 = swarm.Base64x64; +const Swarm = sync.Swarm; +const ClientMeta = require('./ClientMeta'); +const Client = sync.Client; +const Syncable = sync.Syncable; +const ReplicaId = swarm.ReplicaId; -/** stores subscriptions to a LevelDOWN instance, like - * /Type#id!connid+replica.on '' - * can be abbreviated to /Type#id!connid - * */ -class Switch extends BatchedOpStream { +/** + * Potentially, routes between databases and shards. + * * retrieve db meta and the scheme + * Auth flow: + * 1. accept all streams + * 2. wait for a handshake .on (no more than HS_WAIT_TIME) + * 3. (if OK) request a client record from the db + * 4. (if OK) check the credentials + * 6. (if OK) reinject the handshake .on as an object subscription + * 7. (if OK) assign a conn_id, send back .on, register the stream + */ +class SwitchOpStream extends OpStream { - /** @param {LevelOp} db */ - constructor (db, callback) { + /** @param {LogOpStream} log + * @param {Stamp} db_repl_id + * @param {Object} options + * @param {Function} callback */ + constructor (db_repl_id, log, options, callback) { super(); - this.db = db; - this.streams = new Map(); - this.ssn_ids = new swarm.VV(); - this._on_op_cb = this._on_op.bind(this); - callback && callback(); + this.dbrid = new Stamp(db_repl_id); + this.rid = null; + this.log = log; + this.subs = new Map(); + /** conn id ts indexed? */ + this.conns = new Map(); + /** repl id indexed? */ + this.replid2connid = new Map(); + this.pending = []; + this.options = options || Object.create(null); + this.clock = null; + this.meta = null; + this._debug = options.debug; + log.on(this); + const local_url = 'swarm://'+this.dbrid.origin+'@local/'+this.dbrid.value; + this.pocket = new Client(local_url, {upstream: this}, err => { + if (!err) { + this.clock = this.pocket._clock; + this.meta = this.pocket._meta; + this.rid = new ReplicaId(this.dbrid.origin, this.meta.replicaIdScheme); + } + callback && callback (err); + }); + this.replid2connid.set (this.replicaId, '0'); + this.conns.set( '0', this.pocket ); + } + + get dbId () { + return this.dbrid.value; + } + + get replicaId () { + return this.dbrid.origin; } /*** - * @param {OpStream} client - the client op stream - * @param {Stamp} stream_id + * @param {OpStream} opstream - the client op stream */ - addClient (client, stream_id, reinject) { - // FIXME no concurrent logins; close the old one - this.ssn_ids.add(stream_id); - this.streams.set(stream_id.origin, client); // FIXME use ts, not origin?!! - // stamp, add - client._id = stream_id; - if (reinject) - this._on_op(reinject, client); - client.on(this._on_op_cb); + on (opstream) { + // opstream._dbrid = null; + this.pending.push({ + stream: opstream, + hs: null, + rid: null, + ops: [], + client: null + }); } - /** @param {OpStream|Stamp} id_or_stream */ - removeClient (id_or_stream) { - let id; - if (id_or_stream.constructor===Stamp) { - id = id_or_stream; - } else if (id_or_stream.constructor===String) { - id = new Stamp(id_or_stream); // TODO repl_id - } else { - id = id_or_stream._id; - if (!id) - throw new Error('not a stream'); - if (this.streams.get(id.origin)!==id_or_stream) - throw new Error('stream unknown'); - } - let stream = this.streams.get(id.origin); - if (!stream) - throw new Error('no such stream'); - stream.off(this._on_op_cb); - this.streams.delete(stream._id.origin); - this.ssn_ids.remove(stream._id.origin); - stream.end(); + off (opstream) { + const dbrid = opstream._dbrid; + if (!dbrid || this.repl2conn.get(dbrid)!==opstream) + return console.warn('unknown stream'); + const connid = this.repl2conn.get(dbrid); + this.replid2connid.delete(dbrid.origin); + this.conns.delete(connid); + opstream._apply(null, this); } - _process_op (op, done) { + /** an op comes from the PeerOpStream */ + _apply (op, _log) { + + if (this._debug) + console.warn(this._debug+'<'+'\t'+op); + if (op.isOnOff()) { - this._process_on_off(op, done); - } else if (op.spec.isScoped()) { - let stream = this.streams.get(op.scope); - stream && this._batch({op:op, streams:[stream]}); - done(); - } else { - return this._process_fan_out(op, done); - } - } - _process_on_off (op, done) { - - let spec = op.spec; - let record = new Spec([spec.Type, spec.Id, spec.scope, Stamp.ZERO]); - let stream = this.streams.get(spec.scope); - if (!stream) - return done(); - this._batch({op: op, streams:[stream]}); - if (op.isOn()) { // outgoing on => add to the table - this.db.put(new swarm.Op(record, ''), done); - } else { // outgoing off => remove from the table - if (op.spec.class===sync.Swarm.id) { - this.db.del(record, (err) => { - stream.offer(op); // TODO clean up - this.removeClient(stream); - done(); - }); - } else { - this.db.del(record, done); - } - } + const oid = op.object; + const scope = op.scope; + let sub = this.subs.get(oid); + const conn_id = this.replid2connid.get(scope); + if (!conn_id) return; - } + if (op.isOn()) { + if (op.isHandshake()) // send back a timestamp + op = op.stamped(new Stamp(conn_id, this.dbrid.origin)); + if (sub === undefined) + this.subs.set(oid, sub=[]); // TODO typed array impl + if (sub.indexOf(conn_id)===-1) + sub.push(conn_id); + } else if (sub) { + const i = sub.indexOf(conn_id); + i !== -1 && sub.splice(i, 1); + } - _process_fan_out (op, done) { + } - let typeid = op.spec.blank('/#'); + this._emit(op); - let send = {op: op, streams: []}; - this._batch(send); + } - this.db.scan( - typeid, - null, - rec => { - let stream = this.streams.get(rec.spec.time); - if (stream) { - send.streams.push(stream); - } else { - // TODO clear the record - } - }, - done, - {} - ); + _emit_to (conn_id, op) { + const stream = this.conns.get(conn_id); + if (stream) + stream._apply(op); + } + req4stream (stream) { + for(let i=0; i { - send.streams.forEach( - stream => stream.offer(send.op) - ); - }); + console.warn('{'+this._debug+'\t'+op); + if (op.isScoped()) { + const replid = op.scope; + const conn_id = this.replid2connid.get(replid); + const stream = this.conns.get(conn_id); + stream._apply (op); + } else { + const sub = this.subs.get(op.object); + if (sub) + sub.forEach( c => this._emit_to(c, op) ); + } } - _on_op (op, stream) { + /** an op from a downstream */ + offer (op, stream) { + if (this._debug) + console.warn('}'+this._debug+'\t'+op); + // sanity checks - stamps, scopes if (op===null) { - this.removeClient(stream); + // TODO inject .off return; - } - if (sync.Syncable._classes[op.spec.class]===undefined) { + } else if (!stream._id) { + const req = this.req4stream(stream); + if (!req.hs && op.isHandshake() && op.scope) { + req.hs = op; + req.rid = new ReplicaId(op.scope, this.meta.replicaIdScheme); + if (req.stream===this.pocket) + return this._accept(req); + req.client = this.pocket.get( + ClientMeta.RDT.Class, + req.rid.client, + this._auth_client.bind(this, req) + ); + } else if (!req.hs) { + this._deny (req, 'HANDSHAKE FIRST'); + } else { + req.ops.push(op); + } + return; + } else if (Base64x64.isAbnormal(op.class) && stream!==this.pocket) { + op = op.error('PRIVATE CLASS', stream._id.origin); + } else if (!Syncable.getClass(op.class)) { op = op.error('CLASS UNKNOWN', stream._id.origin); } else if (op.isOnOff()) { - let check = this.streams.get(op.spec.scope); - if (check!==stream) + if (stream._id.origin !== op.scope) op = op.error('WRONG SCOPE', stream._id.origin); } else { - let check = this.streams.get(op.origin); - if (check!==stream) + if (stream._id.origin !== op.origin) op = op.error('WRONG ORIGIN', stream._id.origin); } - this._emit(op); // preserve batching + + if (this._debug) + console.warn(this._debug+'>'+'\t'+op); + + this.log.offer(op); + + } + + _deny (hs_obj, message) { + const i = this.pending.indexOf(hs_obj); + if (i===-1) throw new Error('no such request'); + this.pending.splice(i, 1); + if (hs_obj.hs) + hs_obj.stream._apply(hs_obj.hs.error(message)); + hs_obj.stream._apply(null); } + _accept (req) { + const i = this.pending.indexOf(req); + if (i===-1) throw new Error('no such request'); + this.pending.splice(i, 1); + const hs = req.hs; + const now = this.clock.issueTimestamp().value; + req.rid.peer = this.rid.peer; + req.rid.session = '0000000001'; + const rid = 'Rclient001'; // req.rid.toString(); + req.stream._id = new Stamp(this.dbrid.value, rid); + const prev_conn_id = this.replid2connid.get(rid); + if (prev_conn_id) { + const prev_stream = this.conns.get(prev_conn_id); + prev_stream._apply(null); + this.conns.delete(prev_conn_id); + } + this.replid2connid.set(rid, now); + this.conns.set(now, req.stream); + if (this._debug) + console.warn(this._debug+'>'+'\t'+hs+' ['+req.ops.length+']'); + this.log.offer(hs); + if (req.ops.length) + this.log.offerAll(req.ops); + } + + _auth_client (req) { + const client = req.client; + req.hs.value; + if (client.hasState()) { + this._accept(req); + } else { + this._deny(req, 'INVALID CREDENTIALS'); + } + client.close(); + } close (callback) { - this._stop(); this._emit(null); - for(var stream of this.streams.values()) - stream.off(this._on_op_cb); - this.db.close(callback); + this.repl2conn.forEach( (ri, ci) => this.offClientMeta(ri) ); } } -module.exports = Switch; + +module.exports = SwitchOpStream; diff --git a/swarm-peer/test/01_switch.js b/swarm-peer/test/01_switch.js index 57df565..054cfc4 100644 --- a/swarm-peer/test/01_switch.js +++ b/swarm-peer/test/01_switch.js @@ -2,63 +2,80 @@ const swarm = require('swarm-protocol'); const sync = require('swarm-syncable'); const tap = require('tap').test; -const LevelOp = require('../src/LevelOp'); +const OpStream = sync.OpStream; const SwitchOpStream = require('../src/SwitchOpStream'); -const LevelDOWN = require('leveldown'); -const rimraf = require('rimraf'); const Spec = swarm.Spec; const Op = swarm.Op; +const ZOS = OpStream.ZeroOpStream; -class StashOpStream extends sync.OpStream { - - constructor () { - super(); - this.stash = []; - } - - offer (op) { - this.stash.push(op); - } - -} tap ('peer.01.A switch basic test', function(t) { let ops = swarm.Op.parseFrame([ - '/LWWObject#id!0.on+client', - '/LWWObject#id!now00+client.op', - '/LWWObject#id!now00+client.off+client', - '/LWWObject#id!now01+client.op', + '/Swarm#test!0.on+R', + '/Swarm#test!now.~+R=\n\t!2.Clock "Logical"\n\t!3.DBIdScheme "0172"', + '/Swarm#test!now.on+R', + '/Swarm#test!0.on+0client Password: 1', + '/~Client#0client!ago.~+R !2.Password 1', + '/~Client#0client!ago.on+R', + '/LWWObject#id!0.on+Rclient001', + '/LWWObject#id!now00+Rclient001.op', + '/LWWObject#id!now00+Rclient001.off+Rclient001', + '/LWWObject#id!now01+Rclient001.op', '' ].join('\n')); - rimraf.sync('.peer.01.A'); - - let client = new StashOpStream(); - let x = null; - let ld = new LevelDOWN('.peer.01.A'); - let db = new LevelOp(ld, {}, () => { - x = new SwitchOpStream(db); - //x._debug = 'S'; - x.addClient(client, new swarm.Stamp("0+client")); - let fake_log = new sync.OpStream(); - x.pipe(fake_log); - fake_log.pipe(x); - client._emitAll(ops); - setTimeout(check, 400); - }); + const sw_on0 = ops.shift(); + const log_meta_op = ops.shift(); + const log_reon = ops.shift(); + const client_on = ops.shift(); + const log_client = ops.shift(); + const log_client_reon = ops.shift(); + + let ready = false; + const client = new ZOS(); + const log = new ZOS(); + const x = new SwitchOpStream( "test+R", log, {debug: 'S'}, err => { + t.equals(err, undefined); + ready=true; + } ); + + t.notOk(ready); + const hson = log.offered.pop(); + t.ok(hson.isHandshake()); + t.equals(hson+'', sw_on0+''); + log._emit(log_meta_op); + t.ok(ready); + log._emit(log_reon); + + x.on(client); + x.offer(client_on, client); + + const client_sw_on = log.offered.pop(); + t.equals(client_sw_on.id, '0client'); + log._emit(log_client); + log._emit(log_client_reon); + + log.offer = function (op) { + this._emit(op); + }; + ops.forEach( o => x.offer(o, client) ); + setTimeout(check, 100); function check() { - let re = client.stash; + let re = client.applied; + console.warn(re.toString()); t.equals(re.length, 3); + t.equals(re[0]+'', ops[0]+''); + t.equals(re[1]+'', ops[1]+''); + t.equals(re[2]+'', ops[2]+''); + //re.forEach(op => console.log(op+'')); t.end(); - rimraf.sync('.peer.01.A'); - } }); \ No newline at end of file diff --git a/swarm-protocol/src/Base64x64.js b/swarm-protocol/src/Base64x64.js index 6c79912..fb81f5a 100644 --- a/swarm-protocol/src/Base64x64.js +++ b/swarm-protocol/src/Base64x64.js @@ -1,8 +1,8 @@ "use strict"; -var base64 = +const base64 = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz~'; -var codes = +const codes = [-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, -1,-1,-1,-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,-1,-1,-1,-1,-1,-1,-1, 10, @@ -11,10 +11,10 @@ var codes = 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62,-1,-1,-1, 63, -1]; -var rs64 = '[0-9A-Za-z_~]'; -var rs64x64 = rs64+'{1,10}'; // 60 bits is enough for everyone -var reTok = new RegExp('^'+rs64x64+'$'); // plain no-extension token -var reNorm64x64 = /^([0-9A-Za-z_~]+?)0*$/; +const rs64 = '[0-9A-Za-z_~]'; +const rs64x64 = rs64+'{1,10}'; // 60 bits is enough for everyone +const reTok = new RegExp('^'+rs64x64+'$'); // plain no-extension token +const reNorm64x64 = /^([0-9A-Za-z_~]+?)0*$/; /** * Base64x64 timestamps are 64-bit timestamps in Base64. @@ -75,7 +75,7 @@ class Base64x64 { reNorm64x64.lastIndex = 0; let m = reNorm64x64.exec(base.toString()); if (m===null) { - throw new Error("not a Base64x64 string"); + throw new Error("not a Base64x64 string: "+base); } return m[1]; } @@ -224,6 +224,10 @@ class Base64x64 { return this._base >= Base64x64.INFINITY; } + static isAbnormal (base) { + return base.charAt(0)==='~'; + } + get ms () { return this.toDate().getTime(); } diff --git a/swarm-syncable/src/Client.js b/swarm-syncable/src/Client.js index ff30c5f..82b98f4 100644 --- a/swarm-syncable/src/Client.js +++ b/swarm-syncable/src/Client.js @@ -30,22 +30,30 @@ class Client extends OpStream { * Create a Client given an upstream and a database id. * Replica id is granted by the upstream. * - * @param {String} url - typeid spec for the database, e.g. `/Swarm#test` or - * `test` or `` for the default database + * @param {String} url - * @param {Object} options - local defaults and overrides for the metadata object */ - constructor (url, options) { + constructor (url, options, callback) { super(); + if (options && options.constructor===Function) { + callback = options; + options = {}; + } + this.options = options || Object.create(null); this._url = new URL(url); - this._id = this._url.replica || '0'; + this._id = new Stamp (this._url.basename, this._url.replica); //:( /** syncables, API objects, the outer state */ this._syncables = Object.create(null); /** we can only init the clock once we have a meta object */ this._clock = null; this._last_acked = Stamp.ZERO; - let next = this._url.clone(); - next.scheme.shift(); - this._upstream = OpStream.connect(next); + this._upstream = this.options.upstream; + if (!this._upstream) { + let next = this._url.clone(); + next.scheme.shift(); + if (!next.scheme.length) throw new Error('upstream not specified'); + this._upstream = OpStream.connect(next); + } this._upstream.on(this); this._unsynced = new Map(); this._meta = this.get( @@ -53,8 +61,9 @@ class Client extends OpStream { this.dbid, state => { // FIXME htis must be state!!! this._clock = new swarm.Clock(state.scope, this._meta.filterByPrefix('Clock')); - this._id = state.scope; + this._id = new Stamp(state.birth, state.scope); this._clock.seeTimestamp(state.Stamp); + callback && callback(); } ); this._meta.onceSync ( @@ -64,6 +73,10 @@ class Client extends OpStream { Syncable.defaultHost = this; } + get replicaId () { + return this._id.origin; + } + get dbid () { return this._url.basename; } @@ -78,7 +91,9 @@ class Client extends OpStream { /** Inject an op. */ _apply (op) { - const rdt = this._syncables[op.object]._rdt; + const syncable = this._syncables[op.object]; + if (!syncable) return; + const rdt = syncable._rdt; if (!op.Stamp.isAbnormal() && this._clock) this._clock.seeTimestamp(op.Stamp); if (op.isOnOff()) @@ -87,7 +102,7 @@ class Client extends OpStream { this._last_acked = op.Stamp; } else { if (!rdt && op.name !== "off") - this._upstream.offer(new Op(op.rename('off'), '')); + this._upstream.offer(new Op(op.renamed('off', this.replicaId), ''), this); else rdt._apply(op); this._emit(op); @@ -95,7 +110,7 @@ class Client extends OpStream { } offer (op, source) { - this._upstream.offer(op); + this._upstream.offer(op, this); } close () { @@ -144,9 +159,9 @@ class Client extends OpStream { const state = feed_state===undefined ? '' : fn._init_state(feed_state, stamp, this._clock); const op = new Op( spec, state ); - this._upstream.offer(op); + this._upstream.offer(op, this); const rdt = new fn.RDT(op, this); - this._upstream.offer(rdt.toOnOff(true).scoped(this._id), this); + this._upstream.offer(rdt.toOnOff(true).scoped(this._id.origin), this); return this._syncables[spec.object] = new fn(rdt); } @@ -163,7 +178,7 @@ class Client extends OpStream { if (!fn) throw new Error('unknown syncable type '+spec); const rdt = new fn.RDT(state0, this); - const on = rdt.toOnOff(true).scoped(this._id); + const on = rdt.toOnOff(true).scoped(this._id.origin); if (on.clazz==='Swarm' && this._url.password) on._value = 'Password: '+this._url.password; // FIXME E E const syncable = new fn(rdt, on_state); diff --git a/swarm-syncable/src/OpStream.js b/swarm-syncable/src/OpStream.js index 4a3c255..7d93a09 100644 --- a/swarm-syncable/src/OpStream.js +++ b/swarm-syncable/src/OpStream.js @@ -223,23 +223,38 @@ OpStream.SLOW_DOWN = Symbol('slow'); // TODO relay backpressure OpStream._URL_HANDLERS = Object.create(null); module.exports = OpStream; +/** a test op stream */ class ZeroOpStream extends OpStream { constructor (url, options) { super(); - this.ops = []; - this.url = new URL(url); - if (this.url.host) - OpStream.QUEUES[this.url.host] = this; + if (url) { + this.url = new URL(url); + const host = this.url.host; + if (OpStream.QUEUES[host]) { + return OpStream.QUEUES[host]; + } else { + OpStream.QUEUES[host] = this; + } + } else { + this.url = null; + } + this.ops = this.offered = []; + this.applied = []; } offer (op) { this.ops.push(op); } + _apply (op) { + this.applied.push(op); + } + } OpStream.QUEUES = Object.create(null); OpStream._URL_HANDLERS['0'] = ZeroOpStream; +OpStream.ZeroOpStream = ZeroOpStream; class CallbackOpStream extends OpStream { diff --git a/swarm-syncable/src/URL.js b/swarm-syncable/src/URL.js index 8bc772a..9adce81 100644 --- a/swarm-syncable/src/URL.js +++ b/swarm-syncable/src/URL.js @@ -20,7 +20,6 @@ class URL { this.hostname = m[6]; this.port = m[7] ? parseInt(m[7]) : 0; this.path = m[8]; - this.basename; this.search = m[9]; this.query; this.hash = m[10]; diff --git a/swarm-syncable/test/03_LWWObject.js b/swarm-syncable/test/03_LWWObject.js index 3e76e23..565397a 100644 --- a/swarm-syncable/test/03_LWWObject.js +++ b/swarm-syncable/test/03_LWWObject.js @@ -1,16 +1,24 @@ "use strict"; -let tap = require('tap').test; -let swarm = require('swarm-protocol'); -let Op = swarm.Op; -let LWWObject = require('../src/LWWObject'); -let Clock = swarm.Clock; //require('../src/Clock'); +const tap = require('tape').test; +const swarm = require('swarm-protocol'); +const Op = swarm.Op; +const LWWObject = require('../src/LWWObject'); +const Clock = swarm.Clock; //require('../src/Clock'); +const Stamp = swarm.Stamp; tap ('syncable.03.A LWW object API', function (t) { let clock = new Clock('test', {ClockMode: 'Logical'}); - let lww = new LWWObject(); + const op = new Op([Stamp.ZERO, Stamp.ZERO, Stamp.ZERO, Op.METHOD_STATE], ''); + const ops = []; + + let lww = new LWWObject(new LWWObject.RDT(op, { + _clock: clock, + time: function() {return clock.issueTimestamp();}, + offer: function(op) {ops.push(op)} + })); lww._clock = clock; // get-set field access @@ -19,7 +27,6 @@ tap ('syncable.03.A LWW object API', function (t) { t.equal( lww.get('field'), 'string' ); t.ok( lww.field===undefined ); // no direct field access - let ops = lww.spill(); t.equals(ops.length, 1); t.equals(ops[0].value, '"string"'); t.equals(ops[0].spec.name, 'field'); @@ -36,7 +43,7 @@ let simple_state_op = Op.parseFrame(simple_state_op_str)[0]; tap ('syncable.03.B LWW object RDT parse/serialize', function (t) { - let rdt = new LWWObject.RDT(simple_state_op.value); + let rdt = new LWWObject.RDT(simple_state_op); t.equals(rdt.get("field"), "string"); t.deepEqual(rdt.get("value"), '{"number":31415}'); @@ -51,13 +58,13 @@ tap ('syncable.03.B LWW object RDT parse/serialize', function (t) { tap ('syncable.03.C LWW object concurrent modification', function (t) { - let rdt = new LWWObject.RDT(simple_state_op.value); + let rdt = new LWWObject.RDT(simple_state_op); const concurrent_op = Op.parseFrame( '/LWWObject#createdBy+author!longago+c0ncurrent.field\twrong\n\n' )[0]; - rdt.apply(concurrent_op); + rdt._apply(concurrent_op); t.equals(rdt.get('field'), 'string'); @@ -65,7 +72,7 @@ tap ('syncable.03.C LWW object concurrent modification', function (t) { '/LWWObject#createdBy+author!longago+concurrent.field\tright\n\n' )[0]; - rdt.apply(non_concurrent_op); + rdt._apply(non_concurrent_op); t.equals(rdt.get('field'), 'right'); From a311c3c99ccacec70e441fb231232204f93526ca Mon Sep 17 00:00:00 2001 From: Victor Grishchenko Date: Wed, 12 Oct 2016 13:13:32 +0500 Subject: [PATCH 11/51] ReplicaId edit methods --- swarm-protocol/src/ReplicaId.js | 30 +++++++++++++++++++++++---- swarm-protocol/src/ReplicaIdScheme.js | 7 ++++++- swarm-protocol/test/06_replica_id.js | 5 +++++ 3 files changed, 37 insertions(+), 5 deletions(-) diff --git a/swarm-protocol/src/ReplicaId.js b/swarm-protocol/src/ReplicaId.js index 64d59d7..feb0413 100644 --- a/swarm-protocol/src/ReplicaId.js +++ b/swarm-protocol/src/ReplicaId.js @@ -1,5 +1,6 @@ "use strict"; const Base64x64 = require('./Base64x64'); +const Scheme = require('./ReplicaIdScheme'); /** Replica id, immutable. * https://gritzko.gitbooks.io/swarm-the-protocol/content/replica.html */ @@ -9,21 +10,26 @@ class ReplicaId { * @param {ReplicaIdScheme} scheme */ constructor(id, scheme) { this._id = null; - this._scheme = scheme; + this._scheme = new Scheme( scheme ); this._parts = [null,null,null,null]; let base = null; if (id.constructor===Array) { if (id.length!==4) throw new Error("need all 4 parts"); - this._parts = id.map( (val, p) => scheme.slice(val, p) ); - this._id = scheme.join(this._parts); + this._parts = id.map( (val, p) => this._scheme.slice(val, p) ); + this._rebuild(); } else { base = new Base64x64(id); this._id = base.toString(); - this._parts = scheme.split(this._id); + this._parts = this._scheme.split(this._id); } } + _rebuild () { + this._id = this._scheme.join(this._parts); + return this; + } + /** @param {Array} parts * @param {ReplicaIdScheme} scheme */ static createId (parts, scheme) { @@ -43,6 +49,22 @@ class ReplicaId { get peer () {return this._parts[1];} get client () {return this._parts[2];} get session () {return this._parts[3];} + set primus (base) { + this._parts[0] = this._scheme.slice(base, 0); + return this._rebuild(); + } + set peer (base) { + this._parts[1] = this._scheme.slice(base, 1); + return this._rebuild(); + } + set client (base) { + this._parts[2] = this._scheme.slice(base, 2); + return this._rebuild(); + } + set session (base) { + this._parts[3] = this._scheme.slice(base, 3); + return this._rebuild(); + } isPeer () { return this.client === '0'; diff --git a/swarm-protocol/src/ReplicaIdScheme.js b/swarm-protocol/src/ReplicaIdScheme.js index 4fbe5b5..6f421eb 100644 --- a/swarm-protocol/src/ReplicaIdScheme.js +++ b/swarm-protocol/src/ReplicaIdScheme.js @@ -7,10 +7,11 @@ class ReplicaIdScheme { /** @param {Number|String} formula - scheme formula, e.g. `"0262"`, `181`... */ constructor (formula) { - if (formula===undefined) + if (!formula) formula = ReplicaIdScheme.DEFAULT_SCHEME; if ((formula).constructor===Number) formula = '' + formula; + formula = formula.toString(); if (formula.length===3) formula = '0' + formula; if (!ReplicaIdScheme.FORMAT_RE.test(formula)) @@ -56,6 +57,10 @@ class ReplicaIdScheme { return new Base64x64(ret).toString(); } + slice (base64, part) { + return new Base64x64(base64).slice(this.offset(part), this.length(part)); + } + isPrimusless () { return this.primuses===0; } diff --git a/swarm-protocol/test/06_replica_id.js b/swarm-protocol/test/06_replica_id.js index 96d8969..3331611 100644 --- a/swarm-protocol/test/06_replica_id.js +++ b/swarm-protocol/test/06_replica_id.js @@ -49,6 +49,11 @@ tape ('protocol.06.B replica id', function (tap) { tap.equals(id3.toString(), 'PeeclientS'); tap.ok(Base64x64.is(id3)); + const id4 = new ReplicaId("0client", "163"); + id4.session = '0000000SSN'; + id4.peer = 'R'; + tap.equals( id4+'', 'RclientSSN' ); + tap.end(); }); From b52f4b034b84bece6cc859c672937a6a8f9ad0f5 Mon Sep 17 00:00:00 2001 From: Victor Grishchenko Date: Wed, 12 Oct 2016 14:27:42 +0500 Subject: [PATCH 12/51] SwitchOpStream - ssn assign --- swarm-peer/src/SwitchOpStream.js | 51 +++++++++++++++++++++++++------- swarm-peer/test/01_switch.js | 2 +- swarm-protocol/src/ReplicaId.js | 12 +++++--- 3 files changed, 49 insertions(+), 16 deletions(-) diff --git a/swarm-peer/src/SwitchOpStream.js b/swarm-peer/src/SwitchOpStream.js index 0a26dee..accad11 100644 --- a/swarm-peer/src/SwitchOpStream.js +++ b/swarm-peer/src/SwitchOpStream.js @@ -165,6 +165,8 @@ class SwitchOpStream extends OpStream { return; } else if (!stream._id) { const req = this.req4stream(stream); + if (!req) + throw new Error('unknown stream'); if (!req.hs && op.isHandshake() && op.scope) { req.hs = op; req.rid = new ReplicaId(op.scope, this.meta.replicaIdScheme); @@ -201,24 +203,39 @@ class SwitchOpStream extends OpStream { } _deny (hs_obj, message) { - const i = this.pending.indexOf(hs_obj); - if (i===-1) throw new Error('no such request'); - this.pending.splice(i, 1); if (hs_obj.hs) hs_obj.stream._apply(hs_obj.hs.error(message)); hs_obj.stream._apply(null); } + _assign_ssn (req) { + // TODO getScoped + const max_rid = req.client.get('max_ssn', this.replicaId) || '0'; + const next_rid = this.rid.clone(); + next_rid.client = req.rid.client; + next_rid.session = Base64x64.inc(max_rid); + if (next_rid.session==='0') + return this._deny(req, 'TODO: ssn id recycling'); + const rid = next_rid.toString(); + req.client.set('max_ssn', next_rid.session); //, this.replicaId); + // req.client.setScoped('login', 'ok', rid); + if (this._debug) + console.warn('!'+this._debug+' assign ssn '+rid); + return rid; + } + _accept (req) { - const i = this.pending.indexOf(req); - if (i===-1) throw new Error('no such request'); - this.pending.splice(i, 1); + // conn id, ssn grant const hs = req.hs; const now = this.clock.issueTimestamp().value; - req.rid.peer = this.rid.peer; - req.rid.session = '0000000001'; - const rid = 'Rclient001'; // req.rid.toString(); + let rid; + if (req.rid.peer==='0') { // new ssn grant + rid = this._assign_ssn(req); + } else { + rid = req.rid; + } req.stream._id = new Stamp(this.dbrid.value, rid); + // register the conn const prev_conn_id = this.replid2connid.get(rid); if (prev_conn_id) { const prev_stream = this.conns.get(prev_conn_id); @@ -227,6 +244,7 @@ class SwitchOpStream extends OpStream { } this.replid2connid.set(rid, now); this.conns.set(now, req.stream); + // reinject queued ops if (this._debug) console.warn(this._debug+'>'+'\t'+hs+' ['+req.ops.length+']'); this.log.offer(hs); @@ -236,12 +254,23 @@ class SwitchOpStream extends OpStream { _auth_client (req) { const client = req.client; - req.hs.value; - if (client.hasState()) { + let props; + try { + const creds = req.hs.value; + if (creds && creds[0]==='{') + props = JSON.parse(creds); + else + props = {Password: creds}; + } catch (ex) {} + if (!props || !client.hasState()) { + this._deny(req, 'INVALID CREDENTIALS'); + } else if (props.Password===client.get('Password')) { this._accept(req); } else { this._deny(req, 'INVALID CREDENTIALS'); } + const i = this.pending.indexOf(req); + this.pending.splice(i, 1); client.close(); } diff --git a/swarm-peer/test/01_switch.js b/swarm-peer/test/01_switch.js index 054cfc4..2075a90 100644 --- a/swarm-peer/test/01_switch.js +++ b/swarm-peer/test/01_switch.js @@ -14,7 +14,7 @@ tap ('peer.01.A switch basic test', function(t) { '/Swarm#test!0.on+R', '/Swarm#test!now.~+R=\n\t!2.Clock "Logical"\n\t!3.DBIdScheme "0172"', '/Swarm#test!now.on+R', - '/Swarm#test!0.on+0client Password: 1', + '/Swarm#test!0.on+0client {"Password": 1}', '/~Client#0client!ago.~+R !2.Password 1', '/~Client#0client!ago.on+R', '/LWWObject#id!0.on+Rclient001', diff --git a/swarm-protocol/src/ReplicaId.js b/swarm-protocol/src/ReplicaId.js index feb0413..d9c593e 100644 --- a/swarm-protocol/src/ReplicaId.js +++ b/swarm-protocol/src/ReplicaId.js @@ -50,19 +50,19 @@ class ReplicaId { get client () {return this._parts[2];} get session () {return this._parts[3];} set primus (base) { - this._parts[0] = this._scheme.slice(base, 0); + this._parts[0] = this._scheme.slice(base, 0).toString(); return this._rebuild(); } set peer (base) { - this._parts[1] = this._scheme.slice(base, 1); + this._parts[1] = this._scheme.slice(base, 1).toString(); return this._rebuild(); } set client (base) { - this._parts[2] = this._scheme.slice(base, 2); + this._parts[2] = this._scheme.slice(base, 2).toString(); return this._rebuild(); } set session (base) { - this._parts[3] = this._scheme.slice(base, 3); + this._parts[3] = this._scheme.slice(base, 3).toString(); return this._rebuild(); } @@ -80,6 +80,10 @@ class ReplicaId { this.isClient() && rid.isPeer(); } + clone () { + return new ReplicaId(this.toString(), this._scheme); + } + toString () { return this._id; } From ac8226dc4b5adae1ef5b463ce4d37f21ffe3e79b Mon Sep 17 00:00:00 2001 From: Victor Grishchenko Date: Wed, 12 Oct 2016 23:50:55 +0500 Subject: [PATCH 13/51] PeerOpStream eats LogOS, PatchOS, SwarmDB --- swarm-peer/src/LevelOp.js | 79 ++++++++++++- swarm-peer/src/LogOpStream.js | 127 -------------------- swarm-peer/src/PeerOpStream.js | 210 +++++++++++++++++++++++++++++++++ swarm-peer/src/SwarmDB.js | 117 ------------------ swarm-peer/test/02_log.js | 26 ++-- swarm-syncable/src/OpStream.js | 3 + swarm-syncable/src/Swarm.js | 6 +- 7 files changed, 304 insertions(+), 264 deletions(-) delete mode 100644 swarm-peer/src/LogOpStream.js create mode 100644 swarm-peer/src/PeerOpStream.js delete mode 100644 swarm-peer/src/SwarmDB.js diff --git a/swarm-peer/src/LevelOp.js b/swarm-peer/src/LevelOp.js index fa88c10..fdafc23 100644 --- a/swarm-peer/src/LevelOp.js +++ b/swarm-peer/src/LevelOp.js @@ -1,6 +1,8 @@ "use strict"; const swarm = require('swarm-protocol'); const Spec = swarm.Spec; +const Stamp = swarm.Stamp; +const Op = swarm.Op; /** just a nice thin wrapper for LevelDOWN API */ class LevelOp { @@ -12,7 +14,15 @@ class LevelOp { } options = options || Object.create(null); this._db = db; - db.open(options, callback); + this.vv = null; + db.open(options, err => { + if (err) + return callback(err); + this._read_vv((err, vv) => { + this.vv = vv; + callback(err, this); + }); + }); } /** Scan a db in the given object's range, starting after the stamp, @@ -30,7 +40,7 @@ class LevelOp { const filter = options.filter || null; let limit = options.limit || (1<<30); if (till===null) { - till = from.restamped(swarm.Stamp.ERROR); + till = from.restamped(Stamp.ERROR); } let i = this._db.iterator({ gte: from.toString(), @@ -43,7 +53,7 @@ class LevelOp { let levelop_read_op = (err, key, value) => { let ret; if (key && !err) { - let op = skip_op ? null : new swarm.Op(key, value); + let op = skip_op ? null : new Op(key, value); if (filter===null || filter(op)) { ret = on_op(op, key, value); if (!--limit) @@ -60,11 +70,64 @@ class LevelOp { i.next(levelop_read_op); return i; } + + /** spec: stamp=0=> to the state callback(err, [ops]) */ + getTail (spec, callback) { + const obj_end = spec.restamped(Stamp.ERROR); + const till = spec.stamp; + const i = this._db.iterator({ + gte: spec.toString(), + lt: obj_end.toString(), + keyAsBuffer: false, + valueAsBuffer: false, + reverse: true + }); + const ops = []; + function on_op (err, key, value) { + if (err) // FIXME i.end() !!! + return callback(err); + if (!key) + return callback(null, ops); + const op = new Op(key, value); + if (till!=='0') { + if (op.stamp<=till) + return callback(null, ops); + } else { + if (op.isState()) + return callback(null, ops); + } + ops.push(op); + i.next(on_op); + } + i.next(on_op); + } + + _read_vv (callback) { + const vv = new swarm.VV(); + let i = this._db.iterator({ + gte: '+0', + lte: '+~~~~~~~~~~', + keyAsBuffer: false, + valueAsBuffer: false + }); + const next = (err, key, value) => { + if (err || !key) { + i.end(()=>{}); + callback(err, err ? null : vv); + } else { + vv.addPair(value, key.substr(1)); + i.next(next); + } + }; + i.next(next); + } + /** @param {Array} ops - an array of Op to save * @param {Function} callback */ putAll (ops, callback) { let batch = ops.map(op => new LevelOp.Put(op)); + ops.forEach(op=>batch.push(new LevelOp.VVAdd(op))); this._db.batch(batch, {sync: true}, callback); } @@ -80,7 +143,7 @@ class LevelOp { /** @param {Spec} spec - the key */ get (spec, callback) { this._db.get(spec.toString(), {asBuffer:false}, - (err, value) => callback && callback(err?null:new swarm.Op(spec, value)) ); + (err, value) => callback && callback(err?null:new Op(spec, value)) ); } del (spec, callback) { @@ -117,4 +180,12 @@ LevelOp.Del = class LevelOpDel { } }; +LevelOp.VVAdd = class { + constructor (op) { + this.type = 'put'; + this.key = '+' + op.spec.Stamp.origin; + this.value = op.spec.Stamp.value; + } +}; + module.exports = LevelOp; \ No newline at end of file diff --git a/swarm-peer/src/LogOpStream.js b/swarm-peer/src/LogOpStream.js deleted file mode 100644 index 5973463..0000000 --- a/swarm-peer/src/LogOpStream.js +++ /dev/null @@ -1,127 +0,0 @@ -"use strict"; -const swarm = require('swarm-protocol'); -const sync = require('swarm-syncable'); -const BatchedOpStream = require('./BatchedOpStream'); -const LevelOp = require('./LevelOp'); -const Spec = swarm.Spec; -const Op = swarm.Op; -const VV = swarm.VV; - -class LogOpStream extends BatchedOpStream { - - /** - * @param {SwarmDB} db - database (key-value op storage) - * @param {Function} callback - * */ - constructor (db, callback) { - - super(); - - this.vv = null; - this.tips = new Map(); - this.tip_bottom = '0'; - this.db = db; - - this.db.read_vv( (err, vv) => { - this.vv = vv; - callback(err); - } ); - - } - - _process (done) { - - let save = []; - let emit = this._egress_batch; - - this._processed_batch.reverse().forEach( op => { // FIXME reverse - - if (op.isNormal()) { - this._processMutation(op, save, emit); - } else if (op.isOn()) { - this._processOn(op, save, emit); - } else if (op.isOff()) { - this._processOff(op, save, emit); - } else if (op.isError()) { - emit.push(op); - } else if (op.isState()) { - this._processState(op, save, emit); - } - }); - - this._processed_batch = []; - - this.db.putAll(save, done); - - } - - _processOn (op, save, emit) { - - const spec = op.spec; - - let tip = this.tips.get(op.id); - let top = this.vv.get(op.origin); - - if (spec.Stamp.isZero()) - emit.push(op); - else if (tip>'0' && spec.Stamp.value>tip) - emit.push(op.error('UNKNOWN BASE > '+tip)); - else if (!top || top '+top)); // leaks max stamp - else - emit.push(op); - - } - - _processOff (off, save, emit) { - - if (off.spec.class===sync.Swarm.id) { - const origin = off.scope; - let top = this.vv.get(origin); - off = off.restamped(top); - } - - emit.push(off); - - } - - _processMutation (op, save, emit) { - - const spec = op.spec; - - let top = this.vv.get(op.origin); - - if (top && spec.time<=top) { - emit.push(op.error("OP REPLAY", op.spec.origin)); - return; - } else { - this.vv.add(spec.Stamp); - emit.push(op); - } - - let tip = this.tips.get(op.id) || this.tip_bottom; - - // we guarantee unique monotonous time values by overstamping ops - let save_op = spec.Stamp.value > tip ? - op : op.overstamped(swarm.Base64x64.inc(tip)); - - this.tips.set(op.id, save_op.spec.Stamp.value); - save.push(save_op); - - } - - _processState (state_op, save, emit) { - if (!state_op.spec.Stamp.eq(state_op.spec.Id)) { - emit.push(state_op.error('NO STATE PUSH', state_op.spec.origin)); - } else { - this._processMutation(state_op, save, emit); - } - } - - -} - -LogOpStream.VV_SPEC = new Spec('/VV#~!0.0'); -LogOpStream.VV_PREFIX_LEN = LogOpStream.VV_SPEC.typeid.length+1; - -module.exports = LogOpStream; diff --git a/swarm-peer/src/PeerOpStream.js b/swarm-peer/src/PeerOpStream.js new file mode 100644 index 0000000..a46bf54 --- /dev/null +++ b/swarm-peer/src/PeerOpStream.js @@ -0,0 +1,210 @@ +"use strict"; +const swarm = require('swarm-protocol'); +const sync = require('swarm-syncable'); +const Spec = swarm.Spec; +const Op = swarm.Op; +const OpStream = sync.OpStream; +const Stamp = swarm.Stamp; +const LevelOp = require('./LevelOp'); + +class PeerOpStream extends OpStream { + + /** + * @param {LevelDOWN} db - database (key-value op storage) + * @param {Object} options + * @param {Function} callback + * */ + constructor (db, options, callback) { + + super(); + + this.vv = null; + this.tips = new Map(); + this.tip_bottom = '0'; + this.db = new LevelOp(db, options, err => { + this.vv = this.db.vv; + callback(err, this); + }); + + this.offered_queue = []; + this.save_queue = []; + + this.pending_scans = []; + this.active_scans = []; + + this._save_cb = this._save_batch.bind(this); + + } + + + offer (op) { + if (this._debug) + console.warn('}'+this._debug+'\t'+op); + // .on : create iterator, register + // .op : push to matching iterators + switch (op.spec.method) { + case Op.METHOD_STATE: this._processState(op); break; + case Op.METHOD_ERROR: this._processError(op); break; + case Op.METHOD_ON: this._processOn(op); break; + case Op.METHOD_OFF: this._processOff(op); break; + default: this._processMutation(op); + } + if (!this._handle) + this._handle = setImmediate(this._save_cb); + } + + _save_batch () { + let save = this.save_queue; + this.save_queue = []; + + this.db.putAll(save, err => { + this._handle = null; + this._emitAll(save.map( op => op.clearstamped() )); + if (this.save_queue.length) + this._handle = setImmediate(this._save_cb); + }); + + } + + _processError (err) { + this._emit(err); + } + + _processOn (op) { + + const spec = op.spec; + + let tip = this.tips.get(op.id); + let top = this.vv.get(op.origin); + + if (spec.Stamp.isZero()) + this.queueScan(op); + else if (tip>'0' && spec.Stamp.value>tip) + this._emit(op.error('UNKNOWN BASE > '+tip)); + else if (!top || top '+top)); // leaks max stamp + else + this.queueScan(op); + + } + + _processOff (off) { + + if (off.spec.class===sync.Swarm.id) { + const origin = off.scope; + let top = this.vv.get(origin); + off = off.restamped(top); + } + + this._emit(off); // FIXME order same-source same-object + + } + + _processMutation (op) { + + const spec = op.spec; + + let top = this.vv.get(op.origin); + if (spec.time<=top) { + return this._emit(op.error("OP REPLAY", op.spec.origin)); + } + this.vv.add(spec.Stamp); // FIXME to LevelOp + + let tip = this.tips.get(op.id) || this.tip_bottom; + // we guarantee unique monotonous time values by overstamping ops + if (spec.Stamp.value <= tip) + op = op.overstamped(swarm.Base64x64.inc(tip)); + this.tips.set(op.id, op.spec.Stamp.value); + + this.save_queue.push(op); + + this.active_scans.forEach( scan => { + if (scan.spec.isSameObject(op.spec)) + scan.races.push(op); + }); + + } + + _processState (state_op) { + if (!state_op.spec.Stamp.eq(state_op.spec.Id)) { + this._emit(state_op.error('NO STATE PUSH', state_op.spec.origin)); + } else { + this._processMutation(state_op, this.save, this.forward); + } + } + + _apply (op, source) { + if (op===null) { + const i = this.active_scans.indexOf(source); + this.active_scans.splice(i, 1); + this.queueScan(); + } else { + this._emit(op); + } + } + + queueScan (on) { + if (on) + this.pending_scans.push(on); + while (this.pending_scans.length && + this.active_scans.length o.overstamped(on.scope)).reverse(); + const max = re_ops.length ? re_ops[re_ops.length - 1].Stamp : Stamp.ZERO; + re_ops.push(on.stamped(max)); + } else if (!ops.length) { // object unknown + re_ops = [on]; + } else if (sync_fn) { // make a snapshot + const state = ops.pop(); + const rdt = new sync_fn.RDT(state); + while (ops.length) + rdt.apply(ops.pop().overstamped(on.scope)); + while (races.length) + rdt.apply(races.shift()); + const new_state = rdt.toOp(); + if (!state.spec.Stamp.eq(state.spec.Id)) + this.db.replace(state, new_state); + else + this.db.put(new_state); + re_ops = [new_state, on.stamped(new_state.Stamp)]; + } else { + re_ops = ops.map(o => o.overstamped(on.scope)).reverse(); + const max = re_ops[re_ops.length - 1].Stamp; + re_ops.push(on.stamped(max)); + } + this._emitAll(re_ops); + } + + + +} + +PeerOpStream.VV_SPEC = new Spec('/VV#~!0.0'); +PeerOpStream.VV_PREFIX_LEN = PeerOpStream.VV_SPEC.typeid.length+1; +PeerOpStream.SCAN_CONCURRENCY = 2; + +module.exports = PeerOpStream; + diff --git a/swarm-peer/src/SwarmDB.js b/swarm-peer/src/SwarmDB.js deleted file mode 100644 index 43b7b62..0000000 --- a/swarm-peer/src/SwarmDB.js +++ /dev/null @@ -1,117 +0,0 @@ -"use strict"; -const swarm = require('swarm-protocol'); -const sync = require('swarm-syncable'); -const LevelOp = require ('./LevelOp'); -const Swarm = sync.Swarm; -const Clock = swarm.Clock; -const Spec = swarm.Spec; -const Stamp = swarm.Stamp; -const ReplicaIdScheme = swarm.ReplicaIdScheme; - -/** An op databasse that has a meta-object and a clock. - * (subclasses LevelOp that subclasses LevelDOWN). */ -class SwarmDB extends LevelOp { - - /** - * @param {Stamp} db_replica_id - * @param {LevelDOWN} leveldown - * @param {Object} options - * @param {Function} callback - * - * */ - constructor (db_replica_id, leveldown, options, callback) { - super(leveldown, options, err => err ? callback(err) : this._read_meta(callback)); - this._full_id = db_replica_id; - this._clock = null; - this._meta = null; - this._scheme = null; - } - - _read_meta (done) { - this._meta = new Swarm(); - const from = new Spec([Swarm.id, this._full_id.value, Stamp.ZERO, Stamp.ZERO]); - this.scan( - from, - null, - state => this._meta.offer(state), - err => { - if (err) return done(err); - this.level.get('+'+this._full_id.origin, {asBuffer:false}, - (err, max) => this._create_clock(err, max, done)); - }, - { - reverse: true, - limit: 1, - filter: o => o.spec.method===swarm.Op.METHOD_STATE - } - ); - } - - read_vv (callback) { - const vv = new swarm.VV(); - let i = this.level.iterator({ - gte: '+0', - lte: '+~~~~~~~~~~', - keyAsBuffer: false, - valueAsBuffer: false - }); - const next = (err, key, value) => { - if (err) - return callback(err, null); - if (!key) - return callback(null, vv); - vv.addPair(value, key.substr(1)); - i.next(next); - }; - i.next(next); - } - - _create_clock (err, max, done) { - if (err) return done(err); - let meta = this._meta; - meta.spill(); //FIXME - meta.on(this._on_meta_op.bind(this)); - // create clock - this._clock = new Clock(this._full_id.origin, meta.filterByPrefix('Clock')); - meta._clock = this._clock; - this._clock.seeTimestamp(max); - this._scheme = new ReplicaIdScheme(meta.get(ReplicaIdScheme.DB_OPTION_NAME)); - done (); - } - - putAll (ops, callback) { - //if (op.isSameObject(this._meta._spec)) - // this._meta.offer(op); - let batch = ops.map(op => new LevelOp.Put(op)); - ops.forEach(op=>batch.push(new SwarmDB.VVAdd(op))); - this._db.batch(batch, {sync: true}, callback); - } - - _on_meta_op (op) { - if (op.spec.origin==this._clock.origin) - super.put(op); - } - - get id () {return this._meta.id;} - get Id () { - if (this._Id===null) - this._Id = new swarm.ReplicaId(this.id, this.scheme); - return this._Id; - } - get meta () {return this._meta;} - get clock () {return this._clock;} - get scheme () {return this._scheme;} - - now () {return this._clock.issueTimestamp();} - -} - -SwarmDB.VVAdd = class { - constructor (op) { - this.type = 'put'; - this.key = '+' + op.spec.Stamp.origin; - this.value = op.spec.Stamp.value; - } -}; - -module.exports = SwarmDB; \ No newline at end of file diff --git a/swarm-peer/test/02_log.js b/swarm-peer/test/02_log.js index 4073632..7c3edfd 100644 --- a/swarm-peer/test/02_log.js +++ b/swarm-peer/test/02_log.js @@ -3,9 +3,8 @@ const swarm = require('swarm-protocol'); const sync = require('swarm-syncable'); const tap = require('tap').test; const LevelOp = require('../src/LevelOp'); -const SwarmDB = require('../src/SwarmDB'); const LevelDOWN = require('leveldown'); -const LogOpStream = require('../src/LogOpStream'); +const PeerOpStream = require('../src/PeerOpStream'); const rimraf = require('rimraf'); const Spec = swarm.Spec; const Op = swarm.Op; @@ -18,7 +17,7 @@ class StashOpStream extends sync.OpStream { this.stash = []; } - offer (op) { + _apply (op) { this.stash.push(op); } @@ -39,29 +38,28 @@ tap ('peer.02.A op log append basics', function(t) { let list = []; rimraf.sync('.peer.02.A'); - let db = new SwarmDB("test", new LevelDOWN('.peer.02.A'), {}, () => { - let log = new LogOpStream(db, (err) => { - if (err) { return t.fail() && t.end(); } + const peer = new PeerOpStream(new LevelDOWN('.peer.02.A'), {}, (err, log) => { + if (err) { return t.fail() && t.end(); } - log.pipe(tray); - log.offerAll(ops); - setTimeout(checkTray, 100); - - }); + log._debug = 'P'; + log.on(tray); + log.offerAll(ops); + setTimeout(checkTray, 100); }); + function checkTray () { let emitted = tray.stash; //emitted.forEach(op=>console.log('emit: '+ op.toString())); + console.warn(emitted.join('\n')); t.equals(emitted.length, 5); - t.ok(emitted[2].isError()); - db.scan(new Spec('/LWWObject#test+replica'), null, op=>list.push(op), checkDB); + peer.db.scan(new Spec('/LWWObject#test+replica'), null, op=>list.push(op), checkDB); } @@ -72,7 +70,7 @@ tap ('peer.02.A op log append basics', function(t) { t.equals(list.length, 3); t.end(); - rimraf.sync('.peer.02.A'); + //rimraf.sync('.peer.02.A'); } diff --git a/swarm-syncable/src/OpStream.js b/swarm-syncable/src/OpStream.js index 7d93a09..8e671d2 100644 --- a/swarm-syncable/src/OpStream.js +++ b/swarm-syncable/src/OpStream.js @@ -265,9 +265,12 @@ class CallbackOpStream extends OpStream { throw new Error('callback is not a function'); this._callback = callback; this._once = !!once; + this._in = false; } _apply (op) { + if (this._in) return; + this._in = true; return (this._callback(op)===OpStream.ENOUGH || this._once) ? OpStream.ENOUGH : OpStream.OK; } diff --git a/swarm-syncable/src/Swarm.js b/swarm-syncable/src/Swarm.js index 5631b0b..1730beb 100644 --- a/swarm-syncable/src/Swarm.js +++ b/swarm-syncable/src/Swarm.js @@ -23,8 +23,10 @@ class Swarm extends LWWObject { } get replicaIdScheme () { - if (this._scheme===null) - this._scheme = new ReplicaIdScheme(this.get('DBIdScheme')); + if (this._scheme===null) { + const formula = this.get(ReplicaIdScheme.DB_OPTION_NAME); + this._scheme = new ReplicaIdScheme(formula); + } return this._scheme; } From 70ea110af40376f9da71ec89e04e9703ab6c0c73 Mon Sep 17 00:00:00 2001 From: Victor Grishchenko Date: Thu, 13 Oct 2016 02:02:17 +0500 Subject: [PATCH 14/51] PeerOS patch making tests pass --- swarm-peer/src/ClientMeta.js | 20 +++++ swarm-peer/src/LevelOp.js | 42 ++++------- swarm-peer/src/PatchOpStream.js | 127 -------------------------------- swarm-peer/src/PeerOpStream.js | 20 ++--- swarm-peer/test/02_log.js | 2 +- swarm-peer/test/03_patch.js | 81 +++++++++++--------- swarm-protocol/src/Op.js | 2 +- 7 files changed, 93 insertions(+), 201 deletions(-) create mode 100644 swarm-peer/src/ClientMeta.js delete mode 100644 swarm-peer/src/PatchOpStream.js diff --git a/swarm-peer/src/ClientMeta.js b/swarm-peer/src/ClientMeta.js new file mode 100644 index 0000000..2b35b7c --- /dev/null +++ b/swarm-peer/src/ClientMeta.js @@ -0,0 +1,20 @@ +"use strict"; +const swarm = require('swarm-protocol'); +const sync = require('swarm-syncable'); +const LWWObject = sync.LWWObject; + +class ClientMeta extends LWWObject { + + + +} + +class ClientMetaRDT extends LWWObject.RDT { + +} + +ClientMeta.RDT = ClientMetaRDT; +ClientMeta.RDT.Class = "~Client"; +sync.Syncable.addClass(ClientMeta); + +module.exports = ClientMeta; \ No newline at end of file diff --git a/swarm-peer/src/LevelOp.js b/swarm-peer/src/LevelOp.js index fdafc23..8599eef 100644 --- a/swarm-peer/src/LevelOp.js +++ b/swarm-peer/src/LevelOp.js @@ -61,7 +61,7 @@ class LevelOp { } } if ( !key || err || ret===LevelOp.ENOUGH ) { - i.end(()=>{}); + i.end(()=>{}); // FIXME on_end(err); } else { i.next(levelop_read_op); @@ -73,33 +73,21 @@ class LevelOp { /** spec: stamp=0=> to the state callback(err, [ops]) */ getTail (spec, callback) { - const obj_end = spec.restamped(Stamp.ERROR); - const till = spec.stamp; - const i = this._db.iterator({ - gte: spec.toString(), - lt: obj_end.toString(), - keyAsBuffer: false, - valueAsBuffer: false, - reverse: true - }); const ops = []; - function on_op (err, key, value) { - if (err) // FIXME i.end() !!! - return callback(err); - if (!key) - return callback(null, ops); - const op = new Op(key, value); - if (till!=='0') { - if (op.stamp<=till) - return callback(null, ops); - } else { - if (op.isState()) - return callback(null, ops); - } - ops.push(op); - i.next(on_op); - } - i.next(on_op); + const till = spec.stamp; + this.scan ( + spec, + null, + op => { + if (!op.isState() || till==='0') + ops.push(op); + const enough = till==='0' ? op.isState() : ((op.isScoped()?op.scope:op.stamp)<=till); + console.warn('XXX', till, op.stamp, enough); + return enough ? LevelOp.ENOUGH : undefined; // FIXME + }, + err => callback(err, err?null:ops), + {reverse: true} + ); } diff --git a/swarm-peer/src/PatchOpStream.js b/swarm-peer/src/PatchOpStream.js deleted file mode 100644 index 6b27aff..0000000 --- a/swarm-peer/src/PatchOpStream.js +++ /dev/null @@ -1,127 +0,0 @@ -"use strict"; -const swarm = require('swarm-protocol'); -const sync = require('swarm-syncable'); -const BatchedOpStream = require('./BatchedOpStream'); -const LevelOp = require('./LevelOp'); -const Op = swarm.Op; -const Spec = swarm.Spec; -const Stamp = swarm.Stamp; - -/** Accepts a stream of ops and subscriptions; mixes patches into the stream, - * replaces (un)subscriptions with reciprocal (un)subscriptions. */ -class PatchOpStream extends BatchedOpStream { - - constructor (db, callback) { - super(); - this._tips_ref = null; - this.db = db; - // TODO - this._snapshotted = Object.create(null); // no tail read needed - callback && callback(); - } - - /** @override */ - _process_op (op, done) { - if (!op.isOn()) { - this._batch(op); - done(); - } else if (op.spec.Stamp.isZero()) { - this._make_snapshot(op, done); - } else { - this._make_tail(op, done); - } - } - - _make_tail (on, done) { - let met = false, last_stamp = Stamp.ZERO; - this.db.scan( - on.spec.rename(Stamp.ZERO), - null, - o => { - if (o.isState() && !o.spec.Id.eq(o.spec.Stamp)) // FIXME ?! - return; - let op = o.clearstamped(on.spec.scope); - if (met) { - this._batch(op); - last_stamp = op.spec.Stamp; - } else if (op.spec.Stamp.eq(on.spec.Stamp)) { - met = true; - last_stamp = op.spec.Stamp; - } - }, - err => { - if (!met) { - this._batch(on.error('NO SUCH OP')); - } else { - this._batch(new Op(on.spec.restamp(last_stamp), '')); - } - done(); - } - ); - - } - - _make_snapshot (on, done) { - - let snapshot = null, tail = []; - const spec = on.spec; - - this.db.scan( - new Spec([spec.Type, spec.Id, Stamp.ZERO, Stamp.ZERO]), - null, - op => { - if (op.isState()) { - snapshot = op; - return LevelOp.ENOUGH; - } else { - tail.push(op); - return undefined; - } - }, - err => { - if (err) - return done(err); - if (snapshot) { - this._batch_snapshot(on, snapshot, tail.reverse(), done); - } else { - this._batch(on); // unknown object - done(); - } - }, - {reverse: true} - ); - - } - - _batch_snapshot (on, snapshot, tail, done) { - const spec = on.spec; - const scope = spec.scope; - if (!tail.length) { - this._batch(snapshot.clearstamped(scope)); - this._batch(on.restamped(snapshot.spec.Stamp)); - return done(); - } - let syncable = sync.Syncable._classes[on.spec.class]; - if (!syncable) { - this._batch(snapshot.clearstamped(scope)); - tail.forEach(op=>this._batch(op.clearstamped(scope))); - this._batch(on.restamped(Stamp.ZERO)); /// FIXME - return done(); - } - let o = new syncable.RDT(snapshot.value); - tail.forEach(op => o.apply(op)); - const last = tail[tail.length-1].clearstamped().spec; - const state = o.toString(); - let new_snapshot = new Op(new Spec([last.Type, last.Id, last.Stamp, new Stamp(Op.METHOD_STATE, '0')]), state); - this._batch(new_snapshot.scoped(on.scope)); - this._batch(on.restamped(new_snapshot.spec.Stamp)); - if (!snapshot.spec.Stamp.eq(snapshot.spec.Id)) - this.db.replace(snapshot, new_snapshot, done); - else - this.db.put(new_snapshot, done); - } - - -} - -module.exports = PatchOpStream; \ No newline at end of file diff --git a/swarm-peer/src/PeerOpStream.js b/swarm-peer/src/PeerOpStream.js index a46bf54..685cc64 100644 --- a/swarm-peer/src/PeerOpStream.js +++ b/swarm-peer/src/PeerOpStream.js @@ -70,7 +70,7 @@ class PeerOpStream extends OpStream { this._emit(err); } - _processOn (op) { + _processOn (op) { // FIXME LATE, NO EARLY!!!!!!!! const spec = op.spec; @@ -152,7 +152,7 @@ class PeerOpStream extends OpStream { const on = this.pending_scans.shift(); const memo = { on, - races: [] + races: [] // FIXME test races }; this.active_scans.push(memo); this.db.getTail(on.spec, this.endScan.bind(this, memo)); @@ -172,26 +172,26 @@ class PeerOpStream extends OpStream { } else if (!on.spec.Stamp.isZero()) { // patch // if (!this.met) // return this._emit(this.on_op.error('NO SUCH OP')); - re_ops = ops.map(o => o.overstamped(on.scope)).reverse(); + re_ops = ops.map(o => o.clearstamped(on.scope)).reverse(); const max = re_ops.length ? re_ops[re_ops.length - 1].Stamp : Stamp.ZERO; re_ops.push(on.stamped(max)); } else if (!ops.length) { // object unknown re_ops = [on]; - } else if (sync_fn) { // make a snapshot + } else if (sync_fn) { // make a snapshot FIXME no state const state = ops.pop(); const rdt = new sync_fn.RDT(state); while (ops.length) - rdt.apply(ops.pop().overstamped(on.scope)); + rdt._apply(ops.pop().clearstamped(on.scope)); while (races.length) - rdt.apply(races.shift()); + rdt._apply(races.shift()); const new_state = rdt.toOp(); if (!state.spec.Stamp.eq(state.spec.Id)) - this.db.replace(state, new_state); + this.db.replace(state, new_state, ()=>{}); else - this.db.put(new_state); - re_ops = [new_state, on.stamped(new_state.Stamp)]; + this.db.put(new_state, ()=>{}); // TODO? + re_ops = [new_state.scoped(on.scope), on.stamped(new_state.Stamp)]; } else { - re_ops = ops.map(o => o.overstamped(on.scope)).reverse(); + re_ops = ops.map(o => o.clearstamped(on.scope)).reverse(); const max = re_ops[re_ops.length - 1].Stamp; re_ops.push(on.stamped(max)); } diff --git a/swarm-peer/test/02_log.js b/swarm-peer/test/02_log.js index 7c3edfd..c6844cc 100644 --- a/swarm-peer/test/02_log.js +++ b/swarm-peer/test/02_log.js @@ -70,7 +70,7 @@ tap ('peer.02.A op log append basics', function(t) { t.equals(list.length, 3); t.end(); - //rimraf.sync('.peer.02.A'); + rimraf.sync('.peer.02.A'); } diff --git a/swarm-peer/test/03_patch.js b/swarm-peer/test/03_patch.js index 91a146d..69befed 100644 --- a/swarm-peer/test/03_patch.js +++ b/swarm-peer/test/03_patch.js @@ -4,7 +4,7 @@ const sync = require('swarm-syncable'); const tap = require('tap').test; const LevelOp = require('../src/LevelOp'); const LevelDOWN = require('leveldown'); -const PatchOpStream = require('../src/PatchOpStream'); +const PeerOpStream = require('../src/PeerOpStream'); const rimraf = require('rimraf'); const Spec = swarm.Spec; const Op = swarm.Op; @@ -13,53 +13,64 @@ const async = require('async'); tap ('peer.03.A patches', function(t) { let ops = swarm.Op.parseFrame([ - '/LWWObject#test+replica!now01+A.key 1', - '/LWWObject#test+replica!now0100001+B.key+now 2', - '/LWWObject#test+replica!now0100001+B.~+now !now+B.key 2', - '/LWWObject#test+replica!now02+A.key 3', + '/LWWObject#0test+replica!0test+replica.~', + '/LWWObject#0test+replica!now01+A.key 1', + '/LWWObject#0test+replica!now0100001+B.key+now 2', + '/LWWObject#0test+replica!now0100001+B.~+now !now+B.key 2', + '/LWWObject#0test+replica!now02+A.key 3', '' ].join('\n')); + let ons = swarm.Op.parseFrame([ - '/LWWObject#test+replica!now+B.on+C', - '/LWWObject#test+replica!0.on+D', + '/LWWObject#0test+replica!now+B.on+C', + '/LWWObject#0test+replica!0.on+D', '' ].join('\n')); - rimraf.sync('.peer.00.A'); - var db, patch; + rimraf.sync('.peer.03.A'); + var patch; + const x = new sync.OpStream.ZeroOpStream(); async.waterfall([ - next => db = new LevelOp( new LevelDOWN('.peer.03.A'), {}, next), - next => db.putAll(ops, next), - next => { patch = new PatchOpStream(db); next(); }, - next => { patch.offerAll(ons); setTimeout(next, 400); }, // FIXME - next => { - let emitted = patch.spill(); - //emitted.forEach(o=>console.log(''+o)); + next => { patch = new PeerOpStream(new LevelDOWN('.peer.03.A'), {}, ()=>next()); }, + next => + patch.db.putAll(ops, next), + next => { + ops.forEach(o=>patch.vv.add(o.Stamp)); + next(); + }, + next => { + patch.on(x); + patch.offerAll(ons); + setTimeout(next, 400); + }, // FIXME + next => { + let emitted = x.applied; + console.warn(emitted.join('\n')); - t.equal(emitted.length, 4); + t.equal(emitted.length, 4); - t.equal(emitted[0].spec.stamp, 'now02+A'); - t.equal(emitted[0].spec.name, 'key+C'); - t.equal(emitted[1].spec.stamp, 'now02+A'); - t.equal(emitted[1].spec.name, 'on+C'); + t.equal(emitted[0].spec.stamp, 'now02+A'); + t.equal(emitted[0].spec.name, 'key+C'); + t.equal(emitted[1].spec.stamp, 'now02+A'); + t.equal(emitted[1].spec.name, 'on+C'); - t.equal(emitted[2].spec.stamp, 'now02+A'); - t.equal(emitted[2].spec.name, '~+D'); - t.equal(emitted[3].spec.stamp, 'now02+A'); - t.equal(emitted[3].spec.name, 'on+D'); + t.equal(emitted[2].spec.stamp, 'now02+A'); + t.equal(emitted[2].spec.name, '~+D'); + t.equal(emitted[3].spec.stamp, 'now02+A'); + t.equal(emitted[3].spec.name, 'on+D'); - next(); - } - //next => db.scan(Spec.ZERO, Spec.ERROR, op=>console.log(':'+op), next) - ], - err => { - rimraf.sync('.peer.03.A'); - if (err) - t.fail(err); - t.end(); - }); + next(); + } + //next => db.scan(Spec.ZERO, Spec.ERROR, op=>console.log(':'+op), next) + ], + err => { + //rimraf.sync('.peer.03.A'); + if (err) + t.fail(err); + t.end(); + }); /* patch.offer(new Op('/LWWObject#test+replica!0.on+C', '')); diff --git a/swarm-protocol/src/Op.js b/swarm-protocol/src/Op.js index ff77286..d39352f 100644 --- a/swarm-protocol/src/Op.js +++ b/swarm-protocol/src/Op.js @@ -152,7 +152,7 @@ class Op extends Spec { return new Op ([ this.Type, this.Id, - new Stamp(this.scope ? this.scope : this.time, this.origin), + new Stamp(this.isScoped() ? this.scope : this.time, this.origin), new Stamp(this.method, new_scope||'0') ], this._value); } From b2b2a5bc08187e59344ae14e378306f0829313da Mon Sep 17 00:00:00 2001 From: Victor Grishchenko Date: Thu, 13 Oct 2016 12:44:47 +0500 Subject: [PATCH 15/51] swarm-peer 1.2.1 --- swarm-peer/index.js | 5 +---- swarm-peer/package.json | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/swarm-peer/index.js b/swarm-peer/index.js index e1dbcf5..9f65e03 100644 --- a/swarm-peer/index.js +++ b/swarm-peer/index.js @@ -3,10 +3,7 @@ module.exports = { LevelOp: require('./src/LevelOp'), - SwarmDB: require('./src/SwarmDB'), - BatchedOpStream: require('./src/BatchedOpStream'), - PatchOpStream: require('./src/PatchOpStream'), - LogOpStream: require('./src/LogOpStream'), + PeerOpStream: require('./src/PeerOpStream'), SwitchOpStream: require('./src/SwitchOpStream') }; diff --git a/swarm-peer/package.json b/swarm-peer/package.json index 7526ff0..523a170 100644 --- a/swarm-peer/package.json +++ b/swarm-peer/package.json @@ -1,6 +1,6 @@ { "name": "swarm-peer", - "version": "1.2.0", + "version": "1.2.1", "homepage": "http://github.com/gritzko/swarm", "repository": { "type": "git", From eed543a7939990164ddf66f135b33b44476aca75 Mon Sep 17 00:00:00 2001 From: Victor Grishchenko Date: Wed, 19 Oct 2016 12:34:00 +0500 Subject: [PATCH 16/51] bat: use tape --- package.json | 4 +++- swarm-bat/Makefile | 2 +- swarm-bat/README.md | 8 +++----- swarm-bat/package.json | 5 ++++- swarm-bat/test/00_parse_format.js | 2 +- 5 files changed, 12 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 2ad7e16..7f884a2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "swarm", - "version": "1.0.16", + "version": "1.2.2", "homepage": "http://github.com/gritzko/swarm", "repository": { "type": "git", @@ -39,6 +39,8 @@ "mkdirp": "^0.5.1", "rimraf": "^2.4.4", "tap": "^2.3.1", + "tape": "^4.6.2", + "tap-diff": "^0.1.1", "jsdoc": "^3.4.0" }, "scripts": { diff --git a/swarm-bat/Makefile b/swarm-bat/Makefile index e6e27ea..c43e9db 100644 --- a/swarm-bat/Makefile +++ b/swarm-bat/Makefile @@ -1,5 +1,5 @@ include ../Makefile.package.in test: - @tap test/??_*.js + @tape test/??_*.js | tap-diff @test/cli-test.sh diff --git a/swarm-bat/README.md b/swarm-bat/README.md index 5bdac37..aaa019d 100644 --- a/swarm-bat/README.md +++ b/swarm-bat/README.md @@ -54,11 +54,9 @@ see test/ for examples of [API](test/00_parse_format.js) and ## Codes -- [ ] 0 OK -- [ ] 1 no match -- [ ] 2 script error -- [ ] 3 argument error -- [ ] 4 io error + 0 OK + 1 test fails +-1 error ## TODO diff --git a/swarm-bat/package.json b/swarm-bat/package.json index 717f995..fd01c16 100644 --- a/swarm-bat/package.json +++ b/swarm-bat/package.json @@ -1,6 +1,6 @@ { "name": "swarm-bat", - "version": "1.2.12", + "version": "1.2.14", "repository": { "type": "git", "url": "https://github.com/gritzko/swarm.git" @@ -30,6 +30,9 @@ "scripts": { "test": "make test" }, + "devDependencies": { + "tape": "4.6.2" + }, "bin": { "bat": "./bat-cli.js" } diff --git a/swarm-bat/test/00_parse_format.js b/swarm-bat/test/00_parse_format.js index e67d28e..95d53b5 100644 --- a/swarm-bat/test/00_parse_format.js +++ b/swarm-bat/test/00_parse_format.js @@ -1,7 +1,7 @@ "use strict"; var bat = require('../bat-api'); const su = require('stream-url'); -var tap = require('tap').test; +var tap = require('tape').test; tap ('1.A parse trivial .batt scripts', function (t) { From 11f0db07ea24f180f4062b266ac72ac6b3b4fff4 Mon Sep 17 00:00:00 2001 From: Victor Grishchenko Date: Wed, 19 Oct 2016 12:35:08 +0500 Subject: [PATCH 17/51] Clock: declare constants --- swarm-protocol/src/Clock.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/swarm-protocol/src/Clock.js b/swarm-protocol/src/Clock.js index dea117a..19a9772 100644 --- a/swarm-protocol/src/Clock.js +++ b/swarm-protocol/src/Clock.js @@ -52,10 +52,10 @@ class Clock { this._logical = false; let options = this._options = meta_options || Object.create(null); if (options.Clock) { - this._logical = options.Clock==='Logical'; + this._logical = options[Clock.OPTION_CLOCK_MODE]==='Logical'; } if (options.ClockLen) { - this._minlen = options.ClockLen; + this._minlen = options[Clock.OPTION_CLOCK_LENGTH]; } if (options.ClockOffst) { this._offset = parseInt(options.ClockOffst); @@ -104,4 +104,7 @@ class Clock { } +Clock.OPTION_CLOCK_LENGTH = "ClockLen"; +Clock.OPTION_CLOCK_MODE = "Clock"; + module.exports = Clock; From 1815ce345360eb03c9ed718091c5904b5680c6bb Mon Sep 17 00:00:00 2001 From: Victor Grishchenko Date: Wed, 19 Oct 2016 12:36:22 +0500 Subject: [PATCH 18/51] ReplicaIdScheme.is --- swarm-protocol/src/ReplicaIdScheme.js | 9 ++++++++- swarm-protocol/src/Spec.js | 5 +++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/swarm-protocol/src/ReplicaIdScheme.js b/swarm-protocol/src/ReplicaIdScheme.js index 6f421eb..7a0e63c 100644 --- a/swarm-protocol/src/ReplicaIdScheme.js +++ b/swarm-protocol/src/ReplicaIdScheme.js @@ -65,6 +65,12 @@ class ReplicaIdScheme { return this.primuses===0; } + static is (scheme) { + if (!ReplicaIdScheme.FORMAT_RE.test(scheme)) return false; + const rids = new ReplicaIdScheme(scheme); + return rids.isCorrect(); + } + isCorrect () { const length = this.primuses+this.peers+this.clients+this.sessions; return length<=10; @@ -103,4 +109,5 @@ ReplicaIdScheme.FORMAT_RE = /^(\d)(\d)(\d)(\d)$/; ReplicaIdScheme.DEFAULT_SCHEME = '0262'; -module.exports = ReplicaIdScheme; \ No newline at end of file +module.exports = ReplicaIdScheme; + diff --git a/swarm-protocol/src/Spec.js b/swarm-protocol/src/Spec.js index 85f5578..df15bec 100644 --- a/swarm-protocol/src/Spec.js +++ b/swarm-protocol/src/Spec.js @@ -215,7 +215,7 @@ class Spec { } isSameObject (spec) { - if (spec.constructor!==Spec) { + if (!spec._toks) { spec = new Spec(spec); } return this.Type.eq(spec.Type) && this.Id.eq(spec.Id); @@ -251,4 +251,5 @@ Spec.NON_SPECIFIC_NOOP = new Spec(); Spec.ZERO = new Spec(); Spec.ERROR = new Spec([Stamp.ERROR, Stamp.ERROR, Stamp.ERROR, Stamp.ERROR]); -module.exports = Spec; \ No newline at end of file +module.exports = Spec; + From e7e0625732082ca594013eb61147383d27f58326 Mon Sep 17 00:00:00 2001 From: Victor Grishchenko Date: Wed, 19 Oct 2016 13:09:08 +0500 Subject: [PATCH 19/51] syncable: small tweaks and bugfixes --- swarm-syncable/src/Client.js | 44 +++++++++++++---------- swarm-syncable/src/OpQueue.js | 62 -------------------------------- swarm-syncable/src/OpStream.js | 20 +++++++---- swarm-syncable/src/Syncable.js | 6 ++-- swarm-syncable/src/URL.js | 9 +++-- swarm-syncable/test/02_Client.js | 4 ++- 6 files changed, 52 insertions(+), 93 deletions(-) delete mode 100644 swarm-syncable/src/OpQueue.js diff --git a/swarm-syncable/src/Client.js b/swarm-syncable/src/Client.js index 82b98f4..ebd7ca8 100644 --- a/swarm-syncable/src/Client.js +++ b/swarm-syncable/src/Client.js @@ -52,7 +52,7 @@ class Client extends OpStream { let next = this._url.clone(); next.scheme.shift(); if (!next.scheme.length) throw new Error('upstream not specified'); - this._upstream = OpStream.connect(next); + this._upstream = OpStream.connect(next, options); } this._upstream.on(this); this._unsynced = new Map(); @@ -98,7 +98,7 @@ class Client extends OpStream { this._clock.seeTimestamp(op.Stamp); if (op.isOnOff()) this._unsynced.delete(op.object); - if (op.origin === this.origin) { + if (!op.isOnOff() && op.origin === this.origin) { // :( this._last_acked = op.Stamp; } else { if (!rdt && op.name !== "off") @@ -110,6 +110,8 @@ class Client extends OpStream { } offer (op, source) { + if (this._debug) + console.warn('}'+this._debug+'>\t'+op); this._upstream.offer(op, this); } @@ -150,18 +152,23 @@ class Client extends OpStream { * @param {Stamp|String|Base64x64} type - the syncable object type * @param feed_state - the initial state */ - create (type, feed_state) { + create (type, feed_state, static_id) { const fn = Syncable.getClass(type); if (!fn) throw new Error('unknown syncable type '+type); const stamp = this.time(); - const spec = new Spec([ type, stamp, stamp, Op.STAMP_STATE ]); + const spec = new Spec([ + fn.RDT.Class, + static_id ? new Stamp(static_id) : stamp, + stamp, + Op.STAMP_STATE + ]); const state = feed_state===undefined ? '' : fn._init_state(feed_state, stamp, this._clock); const op = new Op( spec, state ); this._upstream.offer(op, this); const rdt = new fn.RDT(op, this); - this._upstream.offer(rdt.toOnOff(true).scoped(this._id.origin), this); + this.offer(rdt.toOnOff(true).scoped(this._id.origin), this); return this._syncables[spec.object] = new fn(rdt); } @@ -183,7 +190,7 @@ class Client extends OpStream { on._value = 'Password: '+this._url.password; // FIXME E E const syncable = new fn(rdt, on_state); this._syncables[spec.object] = syncable; - this._upstream.offer(on, this); + this.offer(on, this); this._unsynced.set(spec.object, 1); return syncable; } @@ -248,18 +255,16 @@ class Client extends OpStream { } onSync (callback) { - if (this._unsynced.size===0) { - callback(null); - } else { - this.on(op => { // TODO .on .off - if (this._unsynced.size === 0) { - callback(op); - return OpStream.ENOUGH; - } else { - return OpStream.OK; - } - }); - } + if (this._unsynced.size===0) + return callback(null); + this.on(op => { // TODO .on .off + if (this._unsynced.size === 0) { + callback(op); + return OpStream.ENOUGH; + } else { + return OpStream.OK; + } + }); } static get (type, id) { @@ -280,4 +285,5 @@ class Client extends OpStream { } -module.exports = Client; \ No newline at end of file +module.exports = Client; + diff --git a/swarm-syncable/src/OpQueue.js b/swarm-syncable/src/OpQueue.js deleted file mode 100644 index 17bd968..0000000 --- a/swarm-syncable/src/OpQueue.js +++ /dev/null @@ -1,62 +0,0 @@ -'use strict'; -var EventEmitter = require('eventemitter3'); -var util = require('util'); - -// A very simple FIFO queue that has nothing Op-specific in it actually. -// `limit`: the planned max size of the queue; once `limit` is exceeded -// the queue will keep accepting new data, but it will "complain" (see offer). -function OpQueue (limit) { - this.queue = new Array(limit||32); - this.offset = 0; - this.size = 0; - this.limit = limit || NaN; -} -util.inherits(OpQueue, EventEmitter); -module.exports = OpQueue; - -// Puts an op into the queue. Returns hasSpace() -OpQueue.prototype.offer = function (op) { - this.queue[this.size++] = op; - if (this.size===1) { - this.emit('readable'); - } - return this.hasSpace(); -}; - - -OpQueue.prototype.at = function (i) { - return this.queue[this.offset+i]; -}; - -// Returns whether the current length of the queue is under the limit. -OpQueue.prototype.hasSpace = function () { - return this.length() < this.limit; -}; - -// -OpQueue.prototype.poll = function (op) { - var ret = this.offsetthis.limit && length<(this.limit>>1)) { - 'release mem'; // TODO - } - for(var i=0; i this._emit(op)); } @@ -209,7 +215,7 @@ class OpStream { const fn = OpStream._URL_HANDLERS[top_proto]; if (!fn) throw new Error('unknown protocol: '+top_proto); - return new fn(url, options); + return new fn(url, options||Object.create(null)); } } @@ -258,7 +264,7 @@ OpStream.ZeroOpStream = ZeroOpStream; class CallbackOpStream extends OpStream { - + constructor (callback, once) { super(); if (!callback || callback.constructor!==Function) @@ -267,14 +273,16 @@ class CallbackOpStream extends OpStream { this._once = !!once; this._in = false; } - + _apply (op) { if (this._in) return; this._in = true; - return (this._callback(op)===OpStream.ENOUGH || this._once) ? + const enough = this._callback(op)===OpStream.ENOUGH; + this._in = false; // FIXME + return (enough || this._once) ? OpStream.ENOUGH : OpStream.OK; } - + } @@ -315,7 +323,7 @@ class FilterOpStream extends OpStream { } return !this._negative; } - + _offer () { throw new Error('not implemented'); } diff --git a/swarm-syncable/src/Syncable.js b/swarm-syncable/src/Syncable.js index a84862b..8281b46 100644 --- a/swarm-syncable/src/Syncable.js +++ b/swarm-syncable/src/Syncable.js @@ -47,7 +47,7 @@ class Syncable extends OpStream { } /** Create, apply and emit a new op. - * @param {String} op_name - the operation name (Base64x64, transcendent) + * @param {String} op_name - the operation name (Base64x64, transcendent) * @param {String} op_value - the op value */ _offer (op_name, op_value) { // FIXME BAD!!! const stamp = this._rdt._host.time(); @@ -177,6 +177,8 @@ class Syncable extends OpStream { /** @param {String|Base64x64} type */ static getClass (clazz) { + if (typeof(clazz)==='function') + clazz = clazz.RDT.Class; // :) return Syncable._classes[clazz]; } @@ -253,7 +255,7 @@ class RDT extends OpStream { } /** - * @returns {String} - the serialized state string + * @returns {String} - the serialized state string */ toString () { return ""; diff --git a/swarm-syncable/src/URL.js b/swarm-syncable/src/URL.js index 9adce81..a8fa58b 100644 --- a/swarm-syncable/src/URL.js +++ b/swarm-syncable/src/URL.js @@ -19,7 +19,9 @@ class URL { this.host = m[5]; this.hostname = m[6]; this.port = m[7] ? parseInt(m[7]) : 0; - this.path = m[8]; + this.path = m[8] || ''; + const lastslash = this.path.lastIndexOf('/'); + this.dbid = lastslash===-1 ? this.path : this.path.substr(lastslash+1); this.search = m[9]; this.query; this.hash = m[10]; @@ -70,7 +72,7 @@ URL.RE_URI = new RegExp( "(?:((B)(?:\\:(\\w+))?)@)?" + // credentials "(((?:[^/?#:@\\s]+\\.)*[^/?#:@\\s]+)" + // domain "(?::([0-9]+))?)" + // port - ")" + + ")?" + "(/[^?#'\"\\s]*)?" + // path "(?:\\?([^'\"#\\s]*))?" + // query "(?:#(\\S*))?$") // fragment @@ -79,4 +81,5 @@ URL.RE_URI = new RegExp( ); -module.exports = URL; \ No newline at end of file +module.exports = URL; + diff --git a/swarm-syncable/test/02_Client.js b/swarm-syncable/test/02_Client.js index 5d3baa1..504c30f 100644 --- a/swarm-syncable/test/02_Client.js +++ b/swarm-syncable/test/02_Client.js @@ -63,6 +63,7 @@ tap ( 'syncable.02.B Client add/removeSyncable API', function (t) { t.notOk(synced); t.equals(host.time().toString(), 'time01+ReplicaSSN'); // cache init upstream._emit(ops[2]); + t.ok(synced); // by-value constructor @@ -104,4 +105,5 @@ tap ( 'syncable.02.B Client add/removeSyncable API', function (t) { t.end(); -}); \ No newline at end of file +}); + From 631b9e08e645e1b8e113509cfae8ffe575a40ddb Mon Sep 17 00:00:00 2001 From: Victor Grishchenko Date: Wed, 19 Oct 2016 13:12:17 +0500 Subject: [PATCH 20/51] peer: tweaks and fixes --- swarm-peer/src/LevelOp.js | 11 +++++--- swarm-peer/src/PeerOpStream.js | 44 ++++++++++++++++---------------- swarm-peer/src/SwitchOpStream.js | 10 +++++--- 3 files changed, 35 insertions(+), 30 deletions(-) diff --git a/swarm-peer/src/LevelOp.js b/swarm-peer/src/LevelOp.js index 8599eef..8cfbce7 100644 --- a/swarm-peer/src/LevelOp.js +++ b/swarm-peer/src/LevelOp.js @@ -70,7 +70,7 @@ class LevelOp { i.next(levelop_read_op); return i; } - + /** spec: stamp=0=> to the state callback(err, [ops]) */ getTail (spec, callback) { const ops = []; @@ -89,7 +89,7 @@ class LevelOp { {reverse: true} ); } - + _read_vv (callback) { const vv = new swarm.VV(); @@ -110,7 +110,7 @@ class LevelOp { }; i.next(next); } - + /** @param {Array} ops - an array of Op to save * @param {Function} callback */ putAll (ops, callback) { @@ -176,4 +176,7 @@ LevelOp.VVAdd = class { } }; -module.exports = LevelOp; \ No newline at end of file +function noop() {} + +module.exports = LevelOp; + diff --git a/swarm-peer/src/PeerOpStream.js b/swarm-peer/src/PeerOpStream.js index 685cc64..8d6c777 100644 --- a/swarm-peer/src/PeerOpStream.js +++ b/swarm-peer/src/PeerOpStream.js @@ -22,8 +22,14 @@ class PeerOpStream extends OpStream { this.tips = new Map(); this.tip_bottom = '0'; this.db = new LevelOp(db, options, err => { - this.vv = this.db.vv; - callback(err, this); + if (!err && !this.db.vv) err = 'no vv'; + if (err) { + this._error(err); + } else { + this.vv = this.db.vv; + this._suspend.forEach(op => this.offer(op)); + } + if (callback) callback(err, this); }); this.offered_queue = []; @@ -32,17 +38,20 @@ class PeerOpStream extends OpStream { this.pending_scans = []; this.active_scans = []; + this._suspend = []; this._save_cb = this._save_batch.bind(this); } offer (op) { + if (!this.vv) + return this._suspend.push(op); if (this._debug) console.warn('}'+this._debug+'\t'+op); // .on : create iterator, register // .op : push to matching iterators - switch (op.spec.method) { + switch (op.method) { case Op.METHOD_STATE: this._processState(op); break; case Op.METHOD_ERROR: this._processError(op); break; case Op.METHOD_ON: this._processOn(op); break; @@ -72,16 +81,14 @@ class PeerOpStream extends OpStream { _processOn (op) { // FIXME LATE, NO EARLY!!!!!!!! - const spec = op.spec; - let tip = this.tips.get(op.id); let top = this.vv.get(op.origin); - if (spec.Stamp.isZero()) + if (op.Stamp.isZero()) this.queueScan(op); - else if (tip>'0' && spec.Stamp.value>tip) + else if (tip>'0' && op.Stamp.value>tip) this._emit(op.error('UNKNOWN BASE > '+tip)); - else if (!top || top '+top)); // leaks max stamp else this.queueScan(op); @@ -102,35 +109,29 @@ class PeerOpStream extends OpStream { _processMutation (op) { - const spec = op.spec; - let top = this.vv.get(op.origin); - if (spec.time<=top) { - return this._emit(op.error("OP REPLAY", op.spec.origin)); + if (op.time<=top) { + return this._emit(op.error("OP REPLAY", op.origin)); } - this.vv.add(spec.Stamp); // FIXME to LevelOp + this.vv.add(op.Stamp); // FIXME to LevelOp let tip = this.tips.get(op.id) || this.tip_bottom; // we guarantee unique monotonous time values by overstamping ops - if (spec.Stamp.value <= tip) + if (op.Stamp.value <= tip) op = op.overstamped(swarm.Base64x64.inc(tip)); - this.tips.set(op.id, op.spec.Stamp.value); + this.tips.set(op.id, op.Stamp.value); this.save_queue.push(op); this.active_scans.forEach( scan => { - if (scan.spec.isSameObject(op.spec)) + if (scan.on.isSameObject(op)) scan.races.push(op); }); } _processState (state_op) { - if (!state_op.spec.Stamp.eq(state_op.spec.Id)) { - this._emit(state_op.error('NO STATE PUSH', state_op.spec.origin)); - } else { - this._processMutation(state_op, this.save, this.forward); - } + this._processMutation(state_op, this.save, this.forward); } _apply (op, source) { @@ -207,4 +208,3 @@ PeerOpStream.VV_PREFIX_LEN = PeerOpStream.VV_SPEC.typeid.length+1; PeerOpStream.SCAN_CONCURRENCY = 2; module.exports = PeerOpStream; - diff --git a/swarm-peer/src/SwitchOpStream.js b/swarm-peer/src/SwitchOpStream.js index accad11..72345d5 100644 --- a/swarm-peer/src/SwitchOpStream.js +++ b/swarm-peer/src/SwitchOpStream.js @@ -1,12 +1,11 @@ "use strict"; const swarm = require('swarm-protocol'); const sync = require('swarm-syncable'); -const Spec = swarm.Spec; +// const Spec = swarm.Spec; const Stamp = swarm.Stamp; -const Op = swarm.Op; +// const Op = swarm.Op; const OpStream = sync.OpStream; const Base64x64 = swarm.Base64x64; -const Swarm = sync.Swarm; const ClientMeta = require('./ClientMeta'); const Client = sync.Client; const Syncable = sync.Syncable; @@ -160,7 +159,7 @@ class SwitchOpStream extends OpStream { console.warn('}'+this._debug+'\t'+op); // sanity checks - stamps, scopes - if (op===null) { + if (op===null) { // FIXME STRUCTURE CHECKS // TODO inject .off return; } else if (!stream._id) { @@ -183,6 +182,8 @@ class SwitchOpStream extends OpStream { req.ops.push(op); } return; + } else if (op.isState() && !op.Stamp.eq(op.Id)) { + op = op.error('NO STATE PUSH', op.origin); } else if (Base64x64.isAbnormal(op.class) && stream!==this.pocket) { op = op.error('PRIVATE CLASS', stream._id.origin); } else if (!Syncable.getClass(op.class)) { @@ -195,6 +196,7 @@ class SwitchOpStream extends OpStream { op = op.error('WRONG ORIGIN', stream._id.origin); } + if (this._debug) console.warn(this._debug+'>'+'\t'+op); From 16c6cc2ece2375367a430052a4000d4ae9303297 Mon Sep 17 00:00:00 2001 From: Victor Grishchenko Date: Wed, 19 Oct 2016 19:48:44 +0500 Subject: [PATCH 21/51] swarm server CLI rework (create, db) --- swarm-server/package.json | 10 ++-- swarm-server/src/access.js | 82 -------------------------- swarm-server/src/create.js | 93 ------------------------------ swarm-server/{cli.js => src/swarm} | 19 ++++-- swarm-server/src/swarm-create | 81 ++++++++++++++++++++++++++ swarm-server/src/swarm-db | 80 +++++++++++++++++++++++++ 6 files changed, 182 insertions(+), 183 deletions(-) delete mode 100644 swarm-server/src/access.js delete mode 100644 swarm-server/src/create.js rename swarm-server/{cli.js => src/swarm} (83%) create mode 100755 swarm-server/src/swarm-create create mode 100755 swarm-server/src/swarm-db diff --git a/swarm-server/package.json b/swarm-server/package.json index 152066c..7421776 100644 --- a/swarm-server/package.json +++ b/swarm-server/package.json @@ -1,6 +1,6 @@ { "name": "swarm-server", - "version": "1.2.1", + "version": "1.2.2", "homepage": "http://github.com/gritzko/swarm", "repository": { "type": "git", @@ -19,9 +19,10 @@ "README.md", "index.js" ], - "main": "./index.js", + "main": "./src/api.js", "bin": { - "swarmd": "./cli.js" + "swarm": "./src/cli.js", + "swarm-create": "./src/create.js" }, "dependencies": { "swarm-protocol": "^1.2.0", @@ -30,7 +31,8 @@ "leveldown": "^1.4.6", "daemon": "^1.1.0", "minimist": "^1.2.0", - "swarm-cli": "^1.2.0" + "swarm-cli": "^1.2.0", + "commander": "^2.9.0" }, "devDependencies": { "swarm-bat": "1.0.x", diff --git a/swarm-server/src/access.js b/swarm-server/src/access.js deleted file mode 100644 index b801c2b..0000000 --- a/swarm-server/src/access.js +++ /dev/null @@ -1,82 +0,0 @@ -"use strict"; -const fs = require('fs'); -const path = require('path'); -const leveldown = require('leveldown'); -const swarm = require('swarm-protocol'); -const peer = require('swarm-peer'); -const Spec = swarm.Spec; -const Stamp = swarm.Stamp; - - -function access (home, args, done) { - - let level = new leveldown(home); - let basename = path.basename(home); - let dbid = new Stamp(basename); - - let db = new peer.SwarmDB(dbid, level, null, err => { - if (err) { - done(err); - } else { - - let erase_prefix = args.e || args.erase; - let put_file = args.p || args.put; - let scan_prefix = args.s || args.scan; - const list = args.v || args.vv; - - if (erase_prefix) - erase(db, erase_prefix, done); - - if (put_file) - put(db, put_file, done); - - if (list) - list_vv(db, list_vv, done); - - if (scan_prefix || !(put_file || erase_prefix || list)) - scan(db, scan_prefix||true, done); - // TODO -g get, -O -0 edit options - - } - }); - -} - -function erase (db, prefix, done) { - db.eraseAll(prefix, done); -} - -function scan (db, prefix, done) { - let from, till; - if (prefix===true) { - from = new Spec(); - till = new Spec([Stamp.ERROR, Stamp.ERROR, Stamp.ERROR, Stamp.ERROR]); - } else { - from = new Spec(prefix); - till = null; - } - db.scan( - from, - till, - op=>console.log(op.toString()), - done - ); -} - -function put (db, file, done) { - let frame = fs.readFileSync(file); - let ops = swarm.Op.parseFrame(frame); - if (!ops) - done('syntax error'); // TODO line etc - db.save(ops, done); -} - -function list_vv (db, filter, done) { - db.read_vv((err, vv)=>{ - if (err) return done(err); - vv.map.forEach((v,k)=>console.log(k,v)); - done(); - }); -} - -module.exports = access; \ No newline at end of file diff --git a/swarm-server/src/create.js b/swarm-server/src/create.js deleted file mode 100644 index b974555..0000000 --- a/swarm-server/src/create.js +++ /dev/null @@ -1,93 +0,0 @@ -"use strict"; -const fs = require('fs'); -const path = require('path'); -const leveldown = require('leveldown'); -const swarm = require('swarm-protocol'); -const sync = require('swarm-syncable'); -const peer = require('swarm-peer'); -const Swarm = sync.Swarm; -const ReplicaIdScheme = swarm.ReplicaIdScheme; - -let db, dbname, replid; - -function create (home, args, done) { - - dbname = args.n || args.name; - replid = args.i || args.id; - - // understand db name / replica id / path - if (dbname||replid) { - - if (!swarm.Base64x64.is(dbname)) - return done('malformed db name'); - if (!swarm.Base64x64.is(replid)) - return done('malformed replica id'); - dbname = dbname || 'test'; - replid = replid || '1'; - home = home + '/' + dbname + '-' + replid; - - } else { // parse the path - - let basename = path.basename(home); - let stamp = new swarm.Stamp(basename); - if (stamp.isError()) - return done('invalid dir name pattern'); - dbname = stamp.value; - replid = stamp.origin; - - } - - // understand the id scheme - const scheme_opt = args['o'+ReplicaIdScheme.DB_OPTION_NAME]; - let scheme = new ReplicaIdScheme(scheme_opt); - if (!scheme) - return done('malformed id scheme'); - if (scheme.primuses) - return done('primuses are not supported yet'); - if (replid.length!==scheme.peers) - return done('peer id length does not match the scheme ('+scheme.peers+')'); - - // let's read the options - let opts = Object.create(null); - Object.keys(args). - filter(key=>key[0]==='o'). - map(key=>key.substr(1)). - filter(opt=>swarm.Base64x64.is(opt) && opt!=ReplicaIdScheme.DB_OPTION_NAME). - forEach( - opt => opts[opt] = args['o'+opt] - ); - opts[ReplicaIdScheme.DB_OPTION_NAME] = scheme.toString(); // FIXME timestamp !~ - let options = new sync.Swarm(); - options._clock = new swarm.Clock(replid, opts); - options.setAll(opts); - options._id = new swarm.Stamp(dbname, '0'); //options._clock.issueTimestamp() - - let state = options.toOp(); //.restamped(clock.issueTimestamp()); - - // OK, let's create things - if (!fs.existsSync(home)) { - fs.mkdirSync(home); - } - - let level = leveldown(home); - - db = new peer.LevelOp (level, {errorIfExists: true}, err => { - - if (err) - return done(err); - - const stamp = state.spec.Stamp; - - db.putAll ([state], err => { - if (err) - done(err); - else - level.put('+'+stamp.origin, stamp.value, err=>db.close(done)); - }); - - }); - -} - - -module.exports = create; diff --git a/swarm-server/cli.js b/swarm-server/src/swarm similarity index 83% rename from swarm-server/cli.js rename to swarm-server/src/swarm index 5f5d106..5cfec8f 100755 --- a/swarm-server/cli.js +++ b/swarm-server/src/swarm @@ -2,21 +2,31 @@ "use strict"; const swarm = require('swarm-protocol'); const args = require('minimist')(process.argv.slice(2)); +const cli = require('commander'); +cli + .version('0.0.1') + .command("create [options] ", "create a database").alias('C') + .command("db [options] ", "read/write from/to a database").alias('D') + // .command("fork") + // .command("access") + .command("run [options] ", "run a database (the default)", {isDefault: true}).alias('R') + // .command("user") + .parse(process.argv); + +/* var help = [ '', 'Basic Swarm server. Usage: ', - ' swarm [-C|-F|-A|-R] path/to/database-id [options]', + ' swarm [-C|-F|-A|-R] path/to/dbid-rid [options]', '', '-C --create create a database (dir name == db name)', - ' -n --name dbname database name (default: take from the path)', - ' -i --id XY replica id (default: take from the path)', ' --oXxx="Yyy" set a global database option Xxx to "Yyy"', ' --OXxx="Yyy" set a scoped database option', '-F --fork fork a database', ' -t --to /path/dbname-YZ a path for the new replica', ' -i --id YZ new replica id', - '-A --access access a database', + '-A --access , access a database', ' -s --scan /Type#id!prefix list all records under a prefix', ' -e --erase /Type#id!prefix erase records', ' -p --put filename add ops to the database', @@ -81,3 +91,4 @@ function done (err) { process.on('uncaughtException', function (err) { console.error("UNCAUGHT EXCEPTION", err, err.stack); }); +*/ diff --git a/swarm-server/src/swarm-create b/swarm-server/src/swarm-create new file mode 100755 index 0000000..ad7a761 --- /dev/null +++ b/swarm-server/src/swarm-create @@ -0,0 +1,81 @@ +#!/usr/bin/env node +"use strict"; +const fs = require('fs'); +const path = require('path'); +const cli = require("commander"); +const swarm = require('swarm-protocol'); +const sync = require('swarm-syncable'); +require('./api'); +const Base64x64 = swarm.Base64x64; +// const Swarm = sync.Swarm; +const Stamp = swarm.Stamp; +const Clock = swarm.Clock; +// const Op = swarm.Op; +const ReplicaIdScheme = swarm.ReplicaIdScheme; + +function more_options (kv, opts) { + let i = kv.indexOf('='); + if (i===-1) i = kv.length; + const key = new Base64x64(kv.substr(0,i)); + const val = kv.substr(i+1); + opts[key] = val; + return opts; +} + +cli + .version("0.0.1") + .usage("[options] ") + .option("-c, --clocklen ", "Timestamp minimum length", parseInt) + .option("-l, --logical", "Use sequential (not hybrid) timestamps") + .option("-s, --scheme", "Replica id scheme", /^\d{3,4}$/) + .option("-o, --option ", "Database option", more_options, {}) + .parse(process.argv); + +const db_path = path.resolve(cli.args[0]); +if (fs.existsSync(db_path)) { + console.warn('the dir already exists'); + return 1; +} +fs.mkdirSync(db_path); + +const basename = path.basename(db_path); +if (!Stamp.is(basename)) { + console.warn('not a proper database replica id (dbid-replid): '+basename); + return 2; +} +const dbrid = new Stamp(basename); +const replid = dbrid.origin; +const dbid = dbrid.value; + +// understand the id scheme +const scheme_str = cli.scheme || "0172"; +if (!ReplicaIdScheme.is(scheme_str)) { + console.warn('invalid replica id scheme: '+scheme_str); + return 3; +} +const scheme = new ReplicaIdScheme(scheme_str); + +if (replid.length!==scheme.peers+scheme.primuses) { + console.warn('peer id length does not match the scheme ('+scheme+')'); + return 4; +} + +const host = new sync.Client( + 'swarm+level:'+db_path, + {}, + create_meta +); +// host._upstream._debug = 'L'; +// host._debug = 'H'; + +function create_meta (err) { + const meta = host.meta; + meta.set(ReplicaIdScheme.DB_OPTION_NAME, scheme.toString()); + if (cli.clocklen) + meta.set(Clock.OPTION_CLOCK_LENGTH, cli.clocklen); + if (cli.logical) + meta.set(Clock.OPTION_CLOCK_MODE, "Logical"); + meta.setAll(cli.option); + //host.onSync(90=>console.log('database created')); + console.log('created database', dbid, 'replica', replid); +} diff --git a/swarm-server/src/swarm-db b/swarm-server/src/swarm-db new file mode 100755 index 0000000..108b5ec --- /dev/null +++ b/swarm-server/src/swarm-db @@ -0,0 +1,80 @@ +#!/usr/bin/env node +"use strict"; +const fs = require('fs'); +const path = require('path'); +const cli = require("commander"); +const leveldown = require('leveldown'); +const swarm = require('swarm-protocol'); +const peer = require('swarm-peer'); +const Spec = swarm.Spec; +const Stamp = swarm.Stamp; + +cli + .version("0.0.1") + .usage("[options] ") + .option("-p, --prefix ", "key (spec) prefix") + .option("-w, --write ", "write a value at the exact prefix") + .option("-d, --delete", "delete at the exact prefix") + .option("-x, --delete-all", "delete all values at the prefix") + .option("-v, --vv", "print out the version vector") + .parse(process.argv); + +if (!cli.args[0]) { + console.error('no db specified'); + return 1; +} +const db_path = path.resolve(cli.args[0]); +if (!fs.existsSync(db_path) || !fs.statSync(db_path).isDirectory()) { + console.warn('the dir does not exist'); + return 2; +} + +const level = new leveldown(db_path); +const db = new peer.LevelOp(level, {}, err => { + if (err) return on_err(err); + const prefix = new Spec(cli.prefix); + const till = new Spec([z2e(prefix.Type), z2e(prefix.Id), z2e(prefix.Stamp), z2e(prefix.Name)]); + if (cli.vv) { + db._read_vv((err, vv)=>{ + if (err) return on_err(err); + vv.map.forEach((v,k)=>console.log(k,v)); + }); + } + if (cli.deleteAll) { + const specs = []; + db.scan( + prefix, + till, + op=>specs.push(op.spec), + err=> { + if (err) + on_err(err); + else + db.delAll(specs, on_err); + } + ); + } else if (cli.delete) { + db.del(prefix, on_err); + } else if (cli.write) { + const op = new swarm.Op(prefix, cli.write); + db.put(op, on_err); + } else { + db.scan( + prefix, + till, + op=>console.log(op.toString()), + on_err + ); + } +}); + +function on_err (err) { + if (err) { + console.error(err); + process.exitCode = 3; + } +} + +function z2e (stamp) { + return stamp.isZero() ? Stamp.ERROR : stamp; +} From 383d66a32fa801ef7f2e21fa41994f9844b5c2e0 Mon Sep 17 00:00:00 2001 From: Victor Grishchenko Date: Fri, 21 Oct 2016 10:12:15 +0500 Subject: [PATCH 22/51] add swarm-user, numerous fixes --- swarm-peer/index.js | 3 +- swarm-peer/src/LevelOp.js | 1 - swarm-peer/src/PeerOpStream.js | 8 +- swarm-peer/src/SwitchOpStream.js | 2 +- swarm-protocol/src/Op.js | 6 +- swarm-server/package.json | 8 +- swarm-server/src/swarm | 3 +- swarm-server/src/swarm-create | 6 +- swarm-server/src/{run.js => swarm-run} | 0 swarm-server/src/swarm-user | 150 +++++++++++++++++++++++++ swarm-server/src/user.js | 127 --------------------- swarm-syncable/src/Client.js | 12 +- swarm-syncable/src/OpStream.js | 5 +- swarm-syncable/src/Syncable.js | 2 +- 14 files changed, 183 insertions(+), 150 deletions(-) rename swarm-server/src/{run.js => swarm-run} (100%) create mode 100755 swarm-server/src/swarm-user delete mode 100644 swarm-server/src/user.js diff --git a/swarm-peer/index.js b/swarm-peer/index.js index 9f65e03..e2d7532 100644 --- a/swarm-peer/index.js +++ b/swarm-peer/index.js @@ -4,6 +4,7 @@ module.exports = { LevelOp: require('./src/LevelOp'), PeerOpStream: require('./src/PeerOpStream'), - SwitchOpStream: require('./src/SwitchOpStream') + SwitchOpStream: require('./src/SwitchOpStream'), + ClientMeta: require('./src/ClientMeta') }; diff --git a/swarm-peer/src/LevelOp.js b/swarm-peer/src/LevelOp.js index 8cfbce7..3b0358a 100644 --- a/swarm-peer/src/LevelOp.js +++ b/swarm-peer/src/LevelOp.js @@ -82,7 +82,6 @@ class LevelOp { if (!op.isState() || till==='0') ops.push(op); const enough = till==='0' ? op.isState() : ((op.isScoped()?op.scope:op.stamp)<=till); - console.warn('XXX', till, op.stamp, enough); return enough ? LevelOp.ENOUGH : undefined; // FIXME }, err => callback(err, err?null:ops), diff --git a/swarm-peer/src/PeerOpStream.js b/swarm-peer/src/PeerOpStream.js index 8d6c777..4e60efc 100644 --- a/swarm-peer/src/PeerOpStream.js +++ b/swarm-peer/src/PeerOpStream.js @@ -16,7 +16,7 @@ class PeerOpStream extends OpStream { * */ constructor (db, options, callback) { - super(); + super(options); this.vv = null; this.tips = new Map(); @@ -179,10 +179,12 @@ class PeerOpStream extends OpStream { } else if (!ops.length) { // object unknown re_ops = [on]; } else if (sync_fn) { // make a snapshot FIXME no state - const state = ops.pop(); + const last_op = ops[ops.length-1]; + const state = last_op.method===Op.METHOD_STATE ? + ops.pop() : Op.zeroStateOp(on); const rdt = new sync_fn.RDT(state); while (ops.length) - rdt._apply(ops.pop().clearstamped(on.scope)); + rdt._apply(ops.pop().clearstamped()); while (races.length) rdt._apply(races.shift()); const new_state = rdt.toOp(); diff --git a/swarm-peer/src/SwitchOpStream.js b/swarm-peer/src/SwitchOpStream.js index 72345d5..f5f9406 100644 --- a/swarm-peer/src/SwitchOpStream.js +++ b/swarm-peer/src/SwitchOpStream.js @@ -254,7 +254,7 @@ class SwitchOpStream extends OpStream { this.log.offerAll(req.ops); } - _auth_client (req) { + _auth_client (req) { // FIXME move to ClientMEta!!! const client = req.client; let props; try { diff --git a/swarm-protocol/src/Op.js b/swarm-protocol/src/Op.js index d39352f..b2290d3 100644 --- a/swarm-protocol/src/Op.js +++ b/swarm-protocol/src/Op.js @@ -82,7 +82,7 @@ class Op extends Spec { } return ret; } - + static serializeFrame (ops, prev_spec) { let frame = ''; ops.forEach( op => { @@ -170,6 +170,10 @@ class Op extends Spec { ], this._value); } + static zeroStateOp (spec) { + return new Op([spec.Type, spec.Id, Stamp.ZERO, Op.METHOD_STATE], ''); + } + } Op.NON_SPECIFIC_NOOP = new Op(Spec.NON_SPECIFIC_NOOP, ""); diff --git a/swarm-server/package.json b/swarm-server/package.json index 7421776..3ed2a2a 100644 --- a/swarm-server/package.json +++ b/swarm-server/package.json @@ -1,6 +1,6 @@ { "name": "swarm-server", - "version": "1.2.2", + "version": "1.2.3", "homepage": "http://github.com/gritzko/swarm", "repository": { "type": "git", @@ -22,7 +22,10 @@ "main": "./src/api.js", "bin": { "swarm": "./src/cli.js", - "swarm-create": "./src/create.js" + "swarm-create": "./src/swarm-create", + "swarm-db": "./src/swarm-db", + "swarm-user": "./src/swarm-user", + "swarm-run": "./src/swarm-run" }, "dependencies": { "swarm-protocol": "^1.2.0", @@ -30,7 +33,6 @@ "swarm-peer": "^1.2.0", "leveldown": "^1.4.6", "daemon": "^1.1.0", - "minimist": "^1.2.0", "swarm-cli": "^1.2.0", "commander": "^2.9.0" }, diff --git a/swarm-server/src/swarm b/swarm-server/src/swarm index 5cfec8f..6051e1c 100755 --- a/swarm-server/src/swarm +++ b/swarm-server/src/swarm @@ -9,9 +9,8 @@ cli .command("create [options] ", "create a database").alias('C') .command("db [options] ", "read/write from/to a database").alias('D') // .command("fork") - // .command("access") + .command("user [options] ", "create/edit users").alias('U') .command("run [options] ", "run a database (the default)", {isDefault: true}).alias('R') - // .command("user") .parse(process.argv); /* diff --git a/swarm-server/src/swarm-create b/swarm-server/src/swarm-create index ad7a761..32c8dc3 100755 --- a/swarm-server/src/swarm-create +++ b/swarm-server/src/swarm-create @@ -28,6 +28,7 @@ cli .option("-c, --clocklen ", "Timestamp minimum length", parseInt) .option("-l, --logical", "Use sequential (not hybrid) timestamps") .option("-s, --scheme", "Replica id scheme", /^\d{3,4}$/) + .option("-d, --debug", "trace ops") .option("-o, --option ", "Database option", more_options, {}) .parse(process.argv); @@ -62,11 +63,10 @@ if (replid.length!==scheme.peers+scheme.primuses) { const host = new sync.Client( 'swarm+level:'+db_path, - {}, + {debug: !!cli.debug}, create_meta ); -// host._upstream._debug = 'L'; -// host._debug = 'H'; + function create_meta (err) { const meta = host.meta; diff --git a/swarm-server/src/run.js b/swarm-server/src/swarm-run similarity index 100% rename from swarm-server/src/run.js rename to swarm-server/src/swarm-run diff --git a/swarm-server/src/swarm-user b/swarm-server/src/swarm-user new file mode 100755 index 0000000..63c9cd9 --- /dev/null +++ b/swarm-server/src/swarm-user @@ -0,0 +1,150 @@ +#!/usr/bin/env node +"use strict"; +const fs = require('fs'); +const path = require('path'); +const cli = require("commander"); +const crypto = require('crypto'); +const readline = require('readline'); +const LevelDOWN = require('leveldown'); +const swarm = require('swarm-protocol'); +const peer = require('swarm-peer'); +const sync = require('swarm-syncable'); +const Spec = swarm.Spec; +const Stamp = swarm.Stamp; +const ReplicaId = swarm.ReplicaId; +const ReplicaIdScheme = swarm.ReplicaIdScheme; +const Base64x64 = swarm.Base64x64; +const ClientMeta = peer.ClientMeta; +require('./api'); + +/** + (change or create fixed id) + swarm user + --id 0gritzko + --name "Victor Grishchenko" + --email victor.grishchenko@gmail.com + --password 1 + + (create a new user, algorithmic id, ask for the password) + swarm user + --name "Victor Grishchenko" + --email victor.grishchenko@gmail.com + --password + + (block a user) + swarm user + --id 0gritzko + --block + +*/ + +cli + .version("0.0.1") + .usage("[options] ") + .option("-i, --id ", "client id to create/edit/block") + // .option("-l, --list", "list users") TODO * queries + .option("-s, --show", "print out user's details") + .option("-n, --name ", "a human-readable name for the user") + .option("-p, --password [password]", "user's password") + .option("-h, --hash [salted]", "clientid-salted user password's hash") + .option("-b, --block", "deactivate a user") + .option("-u, --unblock", "reactivate a user") + .option("-d, --debug", "trace ops") + .parse(process.argv); + +if (!cli.args[0]) { + console.error('no db specified'); + return 1; +} +const db_path = path.resolve(cli.args[0]); +if (!fs.existsSync(db_path) || !fs.statSync(db_path).isDirectory()) { + console.warn('the dir does not exist'); + return 2; +} + + +let userid = cli.id; +let user; + +if (!Base64x64.is(userid)) + return on_err('user id must be Base64x64'); + +// FIXME probably, generate id + +const host = new sync.Client('swarm+level:'+db_path, {debug: true}, err => { + + if (err) return on_err(err); + + const scheme = host.meta.replicaIdScheme; + + if (scheme.slice(userid,ReplicaIdScheme.CLIENT)!=userid) + return on_err("user id does not match the db's replica id scheme"); + + user = host.get(ClientMeta, userid, do_things); + +}); + +function on_err (err) { + console.error(err, new Error().stack); + process.exitCode = 1; +} + +function do_things () { + if (cli.name) + user.set('name', cli.name); + if (cli.password) + user.set('password', cli.password); + if (cli.block) + user.set('blocked', true); + if (cli.unblock) + user.set('blocked', false); + + if (cli.show) + console.log(userid + ':\t' + user.toString()+''); + +} + + +function salted_hash (client_id, password) { + const add_salt = crypto.createHash('sha256'); + // TODO stdio cycle + add_salt.update(client_id); + add_salt.update(' '); + add_salt.update(password); + return add_salt.digest('base64'); + +} + +// FIXME read stdin +// const rl = readline.createInterface({ +// input: process.stdin, +// output: process.stdout +// }); +// +// rl.question(login + ' password: ', (password) => {} ); + + +// FIXME open-all +// function list_users (db, args, done) { +// const from = new Spec([Auth.CLIENT_CLASS,Stamp.ZERO,Stamp.ZERO,Stamp.ZERO]); +// const till = new Spec([Auth.CLIENT_CLASS,Stamp.NEVER,Stamp.NEVER,Stamp.NEVER]); +// const offset = db.scheme.partOffset(ReplicaIdScheme.CLIENT); +// let user = ''; +// let status = ''; +// db.scan( from, till, +// o=> { +// let new_user = Base64x64.leftShift(o.spec.Id.value, offset); +// if (user!==new_user) +// console.log(user, status); +// user = new_user; +// status = o.value==='(blocked)' ? 'BLOCKED' : 'ACTIVE'; +// }, +// err => { +// console.log(user, status); +// done(err); +// }, +// { +// filter: o => o.spec.method === Auth.METHOD_PASSWORD +// } +// ); +// } diff --git a/swarm-server/src/user.js b/swarm-server/src/user.js deleted file mode 100644 index 028ac9b..0000000 --- a/swarm-server/src/user.js +++ /dev/null @@ -1,127 +0,0 @@ -"use strict"; -const fs = require('fs'); -const path = require('path'); -const crypto = require('crypto'); -const readline = require('readline'); -const LevelDOWN = require('leveldown'); -const swarm = require('swarm-protocol'); -const peer = require('swarm-peer'); -const Spec = swarm.Spec; -const Stamp = swarm.Stamp; -const ReplicaId = swarm.ReplicaId; -const ReplicaIdScheme = swarm.ReplicaIdScheme; -const Base64x64 = swarm.Base64x64; -const Auth = require('./AuthOpStream'); - -// /~Client#0login!timestamp+R.passwd SHA-256(0login+' '+password) -// /~Client#0login!timestart+R.ssn+RloginSSN // session init -// /~Client#0login!timestamp+R.login+RloginSSN // login record - -function users (home, args, done) { // FIXME parse in cli.js - - let level = new LevelDOWN(home); - let basename = path.basename(home); - if (!Stamp.is(basename)) - return done("can not parse db name/replica id"); - const replica = new Stamp(basename); - - let db = new peer.SwarmDB(replica, level, null, err => { - if (err) { - done(err); - } else { - - let add = args.a || args.add; - let remove = args.r || args.remove; - let list = args.l || args.list; - - if (add) - add_user(db, args, done); - if (remove) - remove_user(db, args, done); - if (list) - list_users(db, args, done); - - } - }); - - -} - -module.exports = users; - -function add_user (db, args, done) { - const login = args.a || args.add; - if (!Base64x64.is(login)) - return done('login must be Base64x64'); - if (login.length>db.scheme.partLength(ReplicaIdScheme.CLIENT)) - return done('too long for the replica id scheme '+db.scheme); - const client = Base64x64.rightShift(login, - db.scheme.partOffset(ReplicaIdScheme.CLIENT)); - - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout - }); - - rl.question(login + ' password: ', (password) => { - const add_salt = crypto.createHash('sha256'); - // TODO stdio cycle - add_salt.update(client); - add_salt.update(' '); - add_salt.update(password); - const salted_hash = add_salt.digest('base64'); - const op = new swarm.Op( new Spec([ - Auth.CLIENT_CLASS, - client, - db.now(), - Auth.METHOD_PASSWORD - ]), salted_hash ); - rl.close(); - db.put(op, done); - }); - -} - -// FIXME big issue: these ops don't go into VV!!! - -function remove_user (db, args, done) { - const login = args.r || args.remove; - if (!Base64x64.is(login)) - return done('login must be Base64x64'); - if (login.length>db.scheme.partLength(ReplicaIdScheme.CLIENT)) - return done('too long for the replica id scheme '+db.scheme); - const client = Base64x64.rightShift(login, - db.scheme.partOffset(ReplicaIdScheme.CLIENT)); - const op = new swarm.Op( new Spec([ - Auth.CLIENT_CLASS, - client, - db.now(), // FIXME origin - Auth.METHOD_PASSWORD - ]), '(blocked)' ); - db.put(op, done); - -} - -function list_users (db, args, done) { - const from = new Spec([Auth.CLIENT_CLASS,Stamp.ZERO,Stamp.ZERO,Stamp.ZERO]); - const till = new Spec([Auth.CLIENT_CLASS,Stamp.NEVER,Stamp.NEVER,Stamp.NEVER]); - const offset = db.scheme.partOffset(ReplicaIdScheme.CLIENT); - let user = ''; - let status = ''; - db.scan( from, till, - o=> { - let new_user = Base64x64.leftShift(o.spec.Id.value, offset); - if (user!==new_user) - console.log(user, status); - user = new_user; - status = o.value==='(blocked)' ? 'BLOCKED' : 'ACTIVE'; - }, - err => { - console.log(user, status); - done(err); - }, - { - filter: o => o.spec.method === Auth.METHOD_PASSWORD - } - ); -} \ No newline at end of file diff --git a/swarm-syncable/src/Client.js b/swarm-syncable/src/Client.js index ebd7ca8..0a0a985 100644 --- a/swarm-syncable/src/Client.js +++ b/swarm-syncable/src/Client.js @@ -34,7 +34,7 @@ class Client extends OpStream { * @param {Object} options - local defaults and overrides for the metadata object */ constructor (url, options, callback) { - super(); + super(options); if (options && options.constructor===Function) { callback = options; options = {}; @@ -89,8 +89,9 @@ class Client extends OpStream { this._meta.onceStateful(callback); } - /** Inject an op. */ _apply (op) { + if (this._debug) + console.warn(this._debug+'{\t'+op); const syncable = this._syncables[op.object]; if (!syncable) return; const rdt = syncable._rdt; @@ -102,7 +103,7 @@ class Client extends OpStream { this._last_acked = op.Stamp; } else { if (!rdt && op.name !== "off") - this._upstream.offer(new Op(op.renamed('off', this.replicaId), ''), this); + this.offer(new Op(op.renamed('off', this.replicaId), ''), this); else rdt._apply(op); this._emit(op); @@ -166,7 +167,7 @@ class Client extends OpStream { const state = feed_state===undefined ? '' : fn._init_state(feed_state, stamp, this._clock); const op = new Op( spec, state ); - this._upstream.offer(op, this); + this.offer(op, this); const rdt = new fn.RDT(op, this); this.offer(rdt.toOnOff(true).scoped(this._id.origin), this); return this._syncables[spec.object] = new fn(rdt); @@ -238,6 +239,8 @@ class Client extends OpStream { id = type; type = LWWObject.Type; } + if (type.constructor === Function) + type = type.RDT.Class; return this.fetch(new Spec([type, id, Stamp.ZERO, Stamp.ZERO]), callback); } @@ -286,4 +289,3 @@ class Client extends OpStream { } module.exports = Client; - diff --git a/swarm-syncable/src/OpStream.js b/swarm-syncable/src/OpStream.js index 5c863c1..7003600 100644 --- a/swarm-syncable/src/OpStream.js +++ b/swarm-syncable/src/OpStream.js @@ -11,11 +11,12 @@ const MUTE=0, ONE_LSTN=1, MANY_LSTN=2, PENDING=3; * */ class OpStream { - constructor () { + constructor (options) { this._lstn = null; /** db replica id: dbname+replica */ this._dbrid = null; - this._debug = null; + this._debug = options && options.debug ? + this.constructor.name.substr(0,1) : null; this.error_message = null; } diff --git a/swarm-syncable/src/Syncable.js b/swarm-syncable/src/Syncable.js index 8281b46..ae56665 100644 --- a/swarm-syncable/src/Syncable.js +++ b/swarm-syncable/src/Syncable.js @@ -168,7 +168,7 @@ class Syncable extends OpStream { } toString () { - return this.toOp().toString(); + return this._rdt.toOp().toString(); } static addClass (fn) { From 79eae628c0114d21ca60ddf3982c96a22a8c8a63 Mon Sep 17 00:00:00 2001 From: Victor Grishchenko Date: Sat, 22 Oct 2016 20:33:44 +0500 Subject: [PATCH 23/51] the first std: handshake with the new OpStream arch --- swarm-peer/src/SwitchOpStream.js | 82 ++++++--- swarm-server/package.json | 3 +- swarm-server/src/LevelPeerOpStream.js | 24 +++ swarm-server/src/NodeServerOpStream.js | 141 ++++++++++++++++ swarm-server/src/api.js | 6 + swarm-server/src/swarm-run | 222 ++++++++----------------- swarm-server/src/swarm-user | 2 +- swarm-syncable/src/Client.js | 23 +-- swarm-syncable/src/Syncable.js | 14 +- 9 files changed, 324 insertions(+), 193 deletions(-) create mode 100644 swarm-server/src/LevelPeerOpStream.js create mode 100644 swarm-server/src/NodeServerOpStream.js create mode 100644 swarm-server/src/api.js mode change 100644 => 100755 swarm-server/src/swarm-run diff --git a/swarm-peer/src/SwitchOpStream.js b/swarm-peer/src/SwitchOpStream.js index f5f9406..be49537 100644 --- a/swarm-peer/src/SwitchOpStream.js +++ b/swarm-peer/src/SwitchOpStream.js @@ -3,7 +3,7 @@ const swarm = require('swarm-protocol'); const sync = require('swarm-syncable'); // const Spec = swarm.Spec; const Stamp = swarm.Stamp; -// const Op = swarm.Op; +const Op = swarm.Op; const OpStream = sync.OpStream; const Base64x64 = swarm.Base64x64; const ClientMeta = require('./ClientMeta'); @@ -30,7 +30,7 @@ class SwitchOpStream extends OpStream { * @param {Object} options * @param {Function} callback */ constructor (db_repl_id, log, options, callback) { - super(); + super(options); this.dbrid = new Stamp(db_repl_id); this.rid = null; this.log = log; @@ -43,17 +43,22 @@ class SwitchOpStream extends OpStream { this.options = options || Object.create(null); this.clock = null; this.meta = null; - this._debug = options.debug; + this.closing = new Map(); log.on(this); - const local_url = 'swarm://'+this.dbrid.origin+'@local/'+this.dbrid.value; - this.pocket = new Client(local_url, {upstream: this}, err => { - if (!err) { - this.clock = this.pocket._clock; - this.meta = this.pocket._meta; - this.rid = new ReplicaId(this.dbrid.origin, this.meta.replicaIdScheme); - } - callback && callback (err); - }); + const local_url = 'swarm://' + this.dbrid.origin + + '@local/' + this.dbrid; + this.pocket = new Client(local_url, { + upstream: this, + debug: options.debug + }, + err => { + if (!err) { + this.clock = this.pocket._clock; + this.meta = this.pocket._meta; + this.rid = new ReplicaId(this.dbrid.origin, this.meta.replicaIdScheme); + } + callback && callback (err); + }); this.replid2connid.set (this.replicaId, '0'); this.conns.set( '0', this.pocket ); } @@ -94,7 +99,7 @@ class SwitchOpStream extends OpStream { _apply (op, _log) { if (this._debug) - console.warn(this._debug+'<'+'\t'+op); + console.warn(this._debug+'{\t'+(op?op:'[EOF]')); if (op.isOnOff()) { @@ -105,8 +110,9 @@ class SwitchOpStream extends OpStream { if (!conn_id) return; if (op.isOn()) { - if (op.isHandshake()) // send back a timestamp - op = op.stamped(new Stamp(conn_id, this.dbrid.origin)); + // FIXME put into the value, don't mess up the semantics + // if (op.isHandshake()) // send back a timestamp + // op = op.stamped(new Stamp(conn_id, this.dbrid.origin)); if (sub === undefined) this.subs.set(oid, sub=[]); // TODO typed array impl if (sub.indexOf(conn_id)===-1) @@ -144,8 +150,18 @@ class SwitchOpStream extends OpStream { if (op.isScoped()) { const replid = op.scope; const conn_id = this.replid2connid.get(replid); - const stream = this.conns.get(conn_id); - stream._apply (op); + if (conn_id) { + const stream = this.conns.get(conn_id); + stream._apply (op, this); + if (op.isHandshake() && op.isOff()) { + this.closing.delete(conn_id); + this.conns.delete(conn_id); // TODO unify + this.replid2connid.delete(replid); + stream._apply(null, this); + } + } else { + console.warn('write to a closed stream: '+replid); + } } else { const sub = this.subs.get(op.object); if (sub) @@ -156,12 +172,21 @@ class SwitchOpStream extends OpStream { /** an op from a downstream */ offer (op, stream) { if (this._debug) - console.warn('}'+this._debug+'\t'+op); + console.warn('}'+this._debug+'\t'+(op?op:'[EOF]')); // sanity checks - stamps, scopes if (op===null) { // FIXME STRUCTURE CHECKS - // TODO inject .off - return; + if (stream._id && !this.closing.has(stream._id)) { + op = new Op([ + this.meta.Type, + this.meta.Id, + this.clock.issueTimestamp(), + new Stamp(Op.METHOD_OFF,stream._id.origin) + ], ''); + this.closing.set(stream._id, Date.now()); + } else { + return; // FIXME state machine unauthd close + } } else if (!stream._id) { const req = this.req4stream(stream); if (!req) @@ -191,14 +216,15 @@ class SwitchOpStream extends OpStream { } else if (op.isOnOff()) { if (stream._id.origin !== op.scope) op = op.error('WRONG SCOPE', stream._id.origin); + else if (op.isHandshake() && op.isOff()) + this.closing.set(stream._id, Date.now()); } else { if (stream._id.origin !== op.origin) op = op.error('WRONG ORIGIN', stream._id.origin); } - if (this._debug) - console.warn(this._debug+'>'+'\t'+op); + console.warn(this._debug+'>\t'+op); this.log.offer(op); @@ -228,7 +254,6 @@ class SwitchOpStream extends OpStream { _accept (req) { // conn id, ssn grant - const hs = req.hs; const now = this.clock.issueTimestamp().value; let rid; if (req.rid.peer==='0') { // new ssn grant @@ -236,7 +261,8 @@ class SwitchOpStream extends OpStream { } else { rid = req.rid; } - req.stream._id = new Stamp(this.dbrid.value, rid); + const hs = req.hs.scoped(rid); + req.stream._id = new Stamp(this.dbrid.value, rid); // TODO check it's dbrid!!! // register the conn const prev_conn_id = this.replid2connid.get(rid); if (prev_conn_id) { @@ -260,20 +286,22 @@ class SwitchOpStream extends OpStream { try { const creds = req.hs.value; if (creds && creds[0]==='{') - props = JSON.parse(creds); + props = JSON.parse(creds); // FIXME report err else props = {Password: creds}; } catch (ex) {} if (!props || !client.hasState()) { + console.warn(props, client._rdt.ops); this._deny(req, 'INVALID CREDENTIALS'); - } else if (props.Password===client.get('Password')) { + } else if (props.password===client.get('password')) { this._accept(req); } else { + console.warn(props.password,client.get('password')); this._deny(req, 'INVALID CREDENTIALS'); } const i = this.pending.indexOf(req); this.pending.splice(i, 1); - client.close(); + // FIXME client.close(); } close (callback) { diff --git a/swarm-server/package.json b/swarm-server/package.json index 3ed2a2a..0d94c61 100644 --- a/swarm-server/package.json +++ b/swarm-server/package.json @@ -1,6 +1,6 @@ { "name": "swarm-server", - "version": "1.2.3", + "version": "1.2.4", "homepage": "http://github.com/gritzko/swarm", "repository": { "type": "git", @@ -34,6 +34,7 @@ "leveldown": "^1.4.6", "daemon": "^1.1.0", "swarm-cli": "^1.2.0", + "duplexify": "^3.4.6", "commander": "^2.9.0" }, "devDependencies": { diff --git a/swarm-server/src/LevelPeerOpStream.js b/swarm-server/src/LevelPeerOpStream.js new file mode 100644 index 0000000..0638368 --- /dev/null +++ b/swarm-server/src/LevelPeerOpStream.js @@ -0,0 +1,24 @@ +"use strict"; +const url = require('url'); +const swarm = require('swarm-protocol'); +const sync = require('swarm-syncable'); +const peer = require('swarm-peer'); +const OpStream = sync.OpStream; +const LevelDOWN = require('leveldown'); +const Swarm = sync.Swarm; +const Op = swarm.Op; +const Stamp = swarm.Stamp; + +class LevelPeerOpStream extends peer.PeerOpStream { + + constructor (db_url, options, callback) { + const path = db_url.path; + const level = new LevelDOWN(path); + super(level, options, callback); + } + +} + +OpStream._URL_HANDLERS['level'] = LevelPeerOpStream; + +module.exports = LevelPeerOpStream; diff --git a/swarm-server/src/NodeServerOpStream.js b/swarm-server/src/NodeServerOpStream.js new file mode 100644 index 0000000..a47655e --- /dev/null +++ b/swarm-server/src/NodeServerOpStream.js @@ -0,0 +1,141 @@ +"use strict"; +const net = require('net'); +const swarm = require('swarm-protocol'); +const sync = require('swarm-syncable'); +const OpStream = sync.OpStream; +const Op = swarm.Op; + +/** An OpStream on top of a Node.js stream. + * Maintains batching guarantees: sends data asynchronously, terminates + * a bundle with \n\n. Expects incoming bundles to be \n\n terminated. */ +class NodeServerOpStream extends OpStream { + + /** @param stream - Node.js stream */ + constructor (stream, options, upstream) { + super(options); + this._upstream = upstream; + this._stream = stream; + stream.setEncoding('utf8'); + this._chunks = []; + this._ops = []; + this._send_to = null; + this._on_data_cb = this._on_data.bind(this); + this._on_end_cb = this._on_end.bind(this); + this._send_cb = this._send.bind(this); + upstream && upstream.on(this); + this._stream.on('data', this._on_data_cb); + this._stream.on('end', this._on_end_cb); + } + + _start () { + } + + _on_data (chunk) { + const chunks = this._chunks; + const had_nl = chunks.length && /\n$/m.test(chunks[chunks.length-1]); + const at = chunk.indexOf('\n\n'); + if (had_nl&&chunk[0]==='\n') { + this._on_batch(); + this._on_data(chunk.substr(1)); + } else if (at!==-1) { + chunks.push(chunk.substr(0, at+2)); + this._on_batch(); + if (at+2 + new NodeServerOpStream(stream, options, upstream) + ); + if (scheme==='tcp') + this._server.listen(url.port, url.hostname); + else if (scheme==='sock') + this._server.listen(url.path); + else + throw new Error('unknown protocol'); + } + } + + close () { + if (this._server) { + this._server.close(); + this._server = null; + } + } +} +NodeServerOpStream.Server = NodeServer; + +OpStream._SERVER_URL_HANDLERS['tcp'] = NodeServer; +OpStream._SERVER_URL_HANDLERS['std'] = NodeServer; +OpStream._SERVER_URL_HANDLERS['sock'] = NodeServer; + +module.exports = NodeServerOpStream; diff --git a/swarm-server/src/api.js b/swarm-server/src/api.js new file mode 100644 index 0000000..4607ff1 --- /dev/null +++ b/swarm-server/src/api.js @@ -0,0 +1,6 @@ +"use strict"; + +module.exports = { + LevelPeerOpStream: require('./LevelPeerOpStream'), + NodeServerOpStream: require('./NodeServerOpStream') +}; diff --git a/swarm-server/src/swarm-run b/swarm-server/src/swarm-run old mode 100644 new mode 100755 index dba2c78..bc90590 --- a/swarm-server/src/swarm-run +++ b/swarm-server/src/swarm-run @@ -1,86 +1,71 @@ +#!/usr/bin/env node "use strict"; const fs = require('fs'); const path = require('path'); const leveldown = require('leveldown'); +const cli = require("commander"); const swarm = require('swarm-protocol'); const sync = require('swarm-syncable'); const peer = require('swarm-peer'); -const async = require('async'); -const NodeOpStream = require('swarm-cli').NodeOpStream; -const AuthOpStream = require('./AuthOpStream'); - -module.exports = function open (home, args, done) { - - if (!fs.existsSync(home)) - return done('no such dir'); - if (!fs.statSync(home).isDirectory()) - return done('not a dir'); - - let sub_home = path.join(home, '.subs'); - const level = leveldown(home); - const sub_level = leveldown(sub_home); - let basename = path.basename(home); - - let db, sub_db; - let switch_stream, log_stream, patch_stream, auth_stream; - - const stages = [ - next => db = new peer.SwarmDB (new swarm.Stamp(basename), level, {createIfMissing: false}, next), - next => sub_db = new peer.LevelOp (sub_level, {createIfMissing: true}, next), - next => switch_stream = new peer.SwitchOpStream(sub_db, next), - next => log_stream = new peer.LogOpStream(db, next), - next => patch_stream = new peer.PatchOpStream(db, next), - next => { - patch_stream.pipe(switch_stream); - log_stream.pipe(patch_stream); - switch_stream.pipe(log_stream); - let trace = args.T || args.trace; - if (trace===true) trace = 'PLS'; - if (trace) { - args.trace = trace; - patch_stream._debug = trace.indexOf('P')===-1 ? null : 'P'; - log_stream._debug = trace.indexOf('L')===-1 ? null : 'L'; - switch_stream._debug = trace.indexOf('S')===-1 ? null : 'S'; - } - next(); - }, - next => auth_stream = new AuthOpStream(db, switch_stream, next), - next => load_auth(args, auth_stream, next), - next => filter(args, log_stream, next), - next => execute(args, next), - next => connect(args, next), - next => listen(args, auth_stream, next), - next => { - if (args.d||args.daemon) - require('daemon')(); - next(); - }, - next => { - process.on('SIGTERM', close); - process.on('SIGINT', close); - process.on('SIGQUIT', close); - } - ]; - - async.waterfall(stages, done); - +const Stamp = swarm.Stamp; +require('./NodeServerOpStream'); + +cli + .version("0.0.1") + .usage("[options] ") + .option("-l, --listen ", "Listen for incoming connections at th URL") + //.option("-c, --connect ", "Connect to a peer at the URL") + .option("-e --exec [script.js]", "execute a script (default: REPL)") + .option("-d, --daemon", "daemonize") + .option("-i --ingest [file.ops]", "ingest ops from a file") + .option("-g --grep ", "grep ongoing log events") + .option("-D, --debug", "debug") + .parse(process.argv); + + +if (!cli.args[0]) { + console.error('no db specified'); + return 1; +} +const db_path = path.resolve(cli.args[0]); +if (!fs.existsSync(db_path) || !fs.statSync(db_path).isDirectory()) + return on_err('the dir does not exist'); +const basename = path.basename(db_path); +if (!Stamp.is(basename)) + return on_err('not a proper database replica id (dbid-replid): '+basename); +const dbrid = new Stamp(basename); + +const options = { + debug: !!cli.debug }; +const level = new leveldown(db_path); +const log_os = new peer.PeerOpStream(level, options); +const switch_os = new peer.SwitchOpStream(dbrid, log_os, options, once_ready); -function close (err) { - // waterfall too - // close inputs, ensure all batches go along the chain, - // close db, close everything +function once_ready (err) { + if (err) return on_err(err); - // in the stdin mode, off triggers close() -} + if (cli.exec) + execute(cli.exec); + + if (cli.grep) + grep(cli.grep); + + if (cli.ingest) + ingest(cli.ingest); + if (cli.daemon) + require('daemon')(); -function execute (args, callback) { - const exec = args.e || args.exec; - if (!exec) { - callback(); - } else if (exec===true) { + if (cli.listen) { + sync.OpStream.listen(cli.listen, options, switch_os); + } + +} + +function execute (exec) { + if (exec===true) { const repl = require('repl'); repl.start({ prompt: process.stdout.isTTY ? '\u2276 ' : '', @@ -94,92 +79,25 @@ function execute (args, callback) { } } -function listen (args, auth_stream, done) { - const listen = args.l || args.listen; - if (!listen) { - done(); - } else if (listen===true || listen==='-') { - listen_stdio(args, auth_stream, done); - } else if (listen.constructor===String) { - - } else if (listen.constructor===Array) { - - } else { - - } - -} - -function listen_stdio (args, auth_stream, done) { - let stdio_stream = new Duplexer(process.stdin, process.stdout); - let opstream = new NodeOpStream(stdio_stream); - if (args.trace && args.trace.indexOf('I')!==-1) - opstream._debug = 'I'; - auth_stream.addClient(opstream, "test"); // FIXME replica id ??!! -} - -function listen_tcp (args, auth_stream, done) { - -} - -function listen_ws (args, auth_stream, done) { - -} - -function load_auth (args, auth_stream, done) { - const req = args.a || args.auth; - let auth_ext; - if (!req) { - done(); - } else if (req.constructor!==String) { - done('auth extension must be an OpStream'); - } else { - let fn = require(req); - auth_ext = new fn(args, done); - auth_ext.connect(auth_stream); +function grep (filter) { + if (filter===true) { + log_os.on(op=>console.log(op.toString())); + } else if (filter.constructor===String) { + log_os.on(filter, op=>console.log(op.toString())); } } -function connect (args, callback) { - const connect = args.c || args.connect; - if (!connect) { - callback(); - } else { - callback(); // TODO peers - } +function ingest (path) { + const file = fs.readFileSync(path); + const ops = swarm.Op.parseFrame(file); + ops.forEach(op => log_os.offer(op)); } -function filter (args, log_stream, callback) { - const filter = args.f || args.filter; - if (!filter) { - } else if (filter===true) { - log_stream.on(op=>console.log(op.toString())); - } else if (filter.constructor===String) { - log_stream.on(filter, op=>console.log(op.toString())); - } - callback(); +function on_err (err) { + console.error('ERROR', err); + process.exitCode = 1; } -const Duplex = require('stream').Duplex; -class Duplexer extends Duplex { - - constructor (reader, writer) { - super(); - this.reader = reader; - this.writer = writer; - this.reader.on('data', data=>this.push(data)); - this.reader.on('end', ()=>this.push(null)); - } - - _write(chunk, encoding, callback) { - this.writer.write(chunk, encoding, callback); - } - - _read(size) { - } - - end () { - setTimeout(process.exit.bind(process), 100); // FIXME proper close - } - -} +// process.on('SIGTERM', on_err.bind('SIGTERM')); TODO graceful close +// process.on('SIGINT', on_err.bind('SIGINT')); +// process.on('SIGQUIT', on_err.bind('SIGQUIT')); diff --git a/swarm-server/src/swarm-user b/swarm-server/src/swarm-user index 63c9cd9..0c37451 100755 --- a/swarm-server/src/swarm-user +++ b/swarm-server/src/swarm-user @@ -49,7 +49,7 @@ cli .option("-h, --hash [salted]", "clientid-salted user password's hash") .option("-b, --block", "deactivate a user") .option("-u, --unblock", "reactivate a user") - .option("-d, --debug", "trace ops") + .option("-D, --debug", "trace ops") .parse(process.argv); if (!cli.args[0]) { diff --git a/swarm-syncable/src/Client.js b/swarm-syncable/src/Client.js index 0a0a985..a6e4b12 100644 --- a/swarm-syncable/src/Client.js +++ b/swarm-syncable/src/Client.js @@ -41,7 +41,7 @@ class Client extends OpStream { } this.options = options || Object.create(null); this._url = new URL(url); - this._id = new Stamp (this._url.basename, this._url.replica); //:( + this._id = new Stamp (this._url.basename); //:( /** syncables, API objects, the outer state */ this._syncables = Object.create(null); /** we can only init the clock once we have a meta object */ @@ -56,6 +56,7 @@ class Client extends OpStream { } this._upstream.on(this); this._unsynced = new Map(); + this._ssn_stamp = this._last_stamp = this._acked_stamp = Stamp.ZERO; this._meta = this.get( SwarmMeta.RDT.Class, this.dbid, @@ -63,6 +64,7 @@ class Client extends OpStream { this._clock = new swarm.Clock(state.scope, this._meta.filterByPrefix('Clock')); this._id = new Stamp(state.birth, state.scope); this._clock.seeTimestamp(state.Stamp); + this._ssn_stamp = this._last_stamp = this._acked_stamp = this.time(); callback && callback(); } ); @@ -78,7 +80,7 @@ class Client extends OpStream { } get dbid () { - return this._url.basename; + return this._id.value; } get origin () { @@ -89,7 +91,7 @@ class Client extends OpStream { this._meta.onceStateful(callback); } - _apply (op) { + _apply (op) { // FIXME chaotic; restructure if (this._debug) console.warn(this._debug+'{\t'+op); const syncable = this._syncables[op.object]; @@ -98,9 +100,9 @@ class Client extends OpStream { if (!op.Stamp.isAbnormal() && this._clock) this._clock.seeTimestamp(op.Stamp); if (op.isOnOff()) - this._unsynced.delete(op.object); - if (!op.isOnOff() && op.origin === this.origin) { // :( - this._last_acked = op.Stamp; + this._unsynced.delete(op.object); // FIXME to RDT + if (!op.isOnOff() && op.origin===this.origin && op.Stamp.gt(this._ssn_stamp)) { // :( + this._acked_stamp = op.Stamp; } else { if (!rdt && op.name !== "off") this.offer(new Op(op.renamed('off', this.replicaId), ''), this); @@ -187,12 +189,12 @@ class Client extends OpStream { throw new Error('unknown syncable type '+spec); const rdt = new fn.RDT(state0, this); const on = rdt.toOnOff(true).scoped(this._id.origin); - if (on.clazz==='Swarm' && this._url.password) - on._value = 'Password: '+this._url.password; // FIXME E E + if (on.isHandshake() && this._url.password) + on._value = JSON.stringify({Password: this._url.password});// FIXME const syncable = new fn(rdt, on_state); this._syncables[spec.object] = syncable; - this.offer(on, this); this._unsynced.set(spec.object, 1); + this.offer(on, this); return syncable; } @@ -220,7 +222,8 @@ class Client extends OpStream { // Returns a new unique timestamp from the host's clock. time () { - return this._clock ? this._clock.issueTimestamp() : null; + if (!this._clock) return null; + return this._last_stamp = this._clock.issueTimestamp(); } /** @return {Swarm} */ diff --git a/swarm-syncable/src/Syncable.js b/swarm-syncable/src/Syncable.js index ae56665..252e49e 100644 --- a/swarm-syncable/src/Syncable.js +++ b/swarm-syncable/src/Syncable.js @@ -25,7 +25,7 @@ class Syncable extends OpStream { * @param {Function} callback - callback to invoke once the object is stateful */ constructor (rdt, callback) { - super(); + super(Syncable.OPTIONS); /** The RDT inner state */ this._rdt = rdt; @@ -52,12 +52,18 @@ class Syncable extends OpStream { _offer (op_name, op_value) { // FIXME BAD!!! const stamp = this._rdt._host.time(); const op = new Op([this.Type, this.Id, stamp, new Stamp(op_name)], op_value); + if (this._debug) + console.warn('}'+this._debug+'#'+(this._rdt?this.id:'x')+ + '\t'+(op?op.toString():'[EOF]')); this._rdt.offer(op); } /** Apply an op to the object's state. * @param {Op} op - the op */ _apply (op) { + if (this._debug) + console.warn(this._debug+'#'+(this._rdt?this.id:'x')+ + '{\t'+(op?op.toString():'[EOF]')); this._rebuild(op); this._emit(op); } @@ -225,7 +231,7 @@ class RDT extends OpStream { this._version = op.Stamp; break; case "off": break; - case "on": + case "on": // cache state kickback if (op.Stamp.isZero() && !this.Version.isZero()) this._host.offer(this.toOp()); break; @@ -290,6 +296,10 @@ Syncable.defaultHost = null; Syncable.addClass(Syncable); +Syncable.OPTIONS = { + debug: 'Z' +}; + module.exports = Syncable; // ----8<---------------------------- From 9b9c15d393bf6439cf83c02de42f91fb5ead4661 Mon Sep 17 00:00:00 2001 From: Victor Grishchenko Date: Mon, 24 Oct 2016 13:17:13 +0500 Subject: [PATCH 24/51] SwitchOpStream and swarm-run clean-up --- swarm-peer/src/SwitchOpStream.js | 88 ++++++++++++++++++-------------- swarm-server/src/swarm-run | 1 + swarm-syncable/src/OpStream.js | 15 +++++- swarm-syncable/src/Syncable.js | 2 +- 4 files changed, 65 insertions(+), 41 deletions(-) diff --git a/swarm-peer/src/SwitchOpStream.js b/swarm-peer/src/SwitchOpStream.js index be49537..4a0457b 100644 --- a/swarm-peer/src/SwitchOpStream.js +++ b/swarm-peer/src/SwitchOpStream.js @@ -174,40 +174,17 @@ class SwitchOpStream extends OpStream { if (this._debug) console.warn('}'+this._debug+'\t'+(op?op:'[EOF]')); - // sanity checks - stamps, scopes - if (op===null) { // FIXME STRUCTURE CHECKS - if (stream._id && !this.closing.has(stream._id)) { - op = new Op([ - this.meta.Type, - this.meta.Id, - this.clock.issueTimestamp(), - new Stamp(Op.METHOD_OFF,stream._id.origin) - ], ''); - this.closing.set(stream._id, Date.now()); - } else { - return; // FIXME state machine unauthd close - } - } else if (!stream._id) { - const req = this.req4stream(stream); - if (!req) - throw new Error('unknown stream'); - if (!req.hs && op.isHandshake() && op.scope) { - req.hs = op; - req.rid = new ReplicaId(op.scope, this.meta.replicaIdScheme); - if (req.stream===this.pocket) - return this._accept(req); - req.client = this.pocket.get( - ClientMeta.RDT.Class, - req.rid.client, - this._auth_client.bind(this, req) - ); - } else if (!req.hs) { - this._deny (req, 'HANDSHAKE FIRST'); - } else { - req.ops.push(op); - } - return; - } else if (op.isState() && !op.Stamp.eq(op.Id)) { + if (op===null) + this._offer_end (null, stream); + else if (!stream._id) + this._offer_new (op, stream); + else + this._offer_op (op, stream); + + } + + _offer_op (op, stream) { + if (op.isState() && !op.Stamp.eq(op.Id)) { op = op.error('NO STATE PUSH', op.origin); } else if (Base64x64.isAbnormal(op.class) && stream!==this.pocket) { op = op.error('PRIVATE CLASS', stream._id.origin); @@ -222,12 +199,47 @@ class SwitchOpStream extends OpStream { if (stream._id.origin !== op.origin) op = op.error('WRONG ORIGIN', stream._id.origin); } - - if (this._debug) - console.warn(this._debug+'>\t'+op); - this.log.offer(op); + } + + _offer_new (op, stream) { + const req = this.req4stream(stream); + if (!req) + throw new Error('unknown stream'); + if (!req.hs && op.isHandshake() && op.scope) { + req.hs = op; + req.rid = new ReplicaId(op.scope, this.meta.replicaIdScheme); + if (req.stream===this.pocket) + return this._accept(req); + req.client = this.pocket.get( + ClientMeta.RDT.Class, + req.rid.client, + this._auth_client.bind(this, req) + ); + } else if (!req.hs) { + this._deny (req, 'HANDSHAKE FIRST'); + } else { + req.ops.push(op); + } + } + _offer_end (_null, stream) { + if (!stream._id) { // purge silently + let i = 0; + const p = this.pending; + while (i Date: Mon, 24 Oct 2016 13:17:46 +0500 Subject: [PATCH 25/51] LWWObject: safer JSON parsing --- swarm-syncable/src/LWWObject.js | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/swarm-syncable/src/LWWObject.js b/swarm-syncable/src/LWWObject.js index 88ebd42..f1a228b 100644 --- a/swarm-syncable/src/LWWObject.js +++ b/swarm-syncable/src/LWWObject.js @@ -42,6 +42,15 @@ class LWWObject extends Syncable { return this._values[name]; } + getScoped (name, scope) { + return this._values[new Stamp(name, scope)]; + } + + setScoped (name, value, scope) { + const event_name = new Stamp(name, scope); + this._offer(event_name, JSON.stringify(value)); + } + has (name) { return this._values.hasOwnProperty(name); } @@ -68,16 +77,26 @@ class LWWObject extends Syncable { if (name===Op.METHOD_STATE) { // rebuild this._values = Object.create(null); this._rdt.ops.forEach(e=>{ - this._values[e.method] = JSON.parse(e.value); + this._values[e.method] = LWWObject.parse(e.value); }); } else if (this._version < op.stamp) { - this._values[name] = JSON.parse(op.value); + this._values[name] = LWWObject.parse(op.value); } else { // reorder const value = this._rdt.get(name); - if (value===undefined) + if (value===undefined) { delete this._values[name]; - else - this._values[name] = JSON.parse(value); + } else { + this._values[name] = LWWObject.parse(value); + } + } + } + + static parse (json) { + try { + return json ? JSON.parse(json) : undefined; + } catch (ex) { + console.warn('Invalid input:', json, '\n', ex); + return undefined; } } From 1a45b442f628e4dfc86493a7c2dd94460170b767 Mon Sep 17 00:00:00 2001 From: Victor Grishchenko Date: Mon, 24 Oct 2016 18:25:45 +0500 Subject: [PATCH 26/51] fix: OpStream must do that itself --- swarm-server/src/NodeServerOpStream.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/swarm-server/src/NodeServerOpStream.js b/swarm-server/src/NodeServerOpStream.js index a47655e..a4f5e92 100644 --- a/swarm-server/src/NodeServerOpStream.js +++ b/swarm-server/src/NodeServerOpStream.js @@ -67,8 +67,6 @@ class NodeServerOpStream extends OpStream { _apply (op) { if (this._debug) console.warn(this._debug+'{\t'+(op?op.toString():'[EOF]')); - if (op===null) - this._upstream.off(this); this._ops.push(op); if (this._send_to===null) this._send_to = setTimeout(this._send_cb, 1); From cdd8179d77e7ba81ae71b594a9fc651470691577 Mon Sep 17 00:00:00 2001 From: Victor Grishchenko Date: Mon, 24 Oct 2016 23:36:39 +0500 Subject: [PATCH 27/51] back to bat testing: first fixes --- swarm-bat/src/StreamTest.js | 10 +++- swarm-peer/src/PeerOpStream.js | 22 ++++--- swarm-peer/src/SwitchOpStream.js | 76 ++++++++++++------------- swarm-protocol/src/Clock.js | 4 ++ swarm-protocol/src/ReplicaIdScheme.js | 8 +-- swarm-protocol/src/VV.js | 17 +++--- swarm-server/src/NodeServerOpStream.js | 3 +- swarm-server/src/swarm | 79 -------------------------- swarm-server/src/swarm-create | 66 +++++++++++++++------ swarm-server/src/swarm-db | 2 +- swarm-server/src/swarm-run | 4 ++ swarm-server/src/swarm-user | 5 +- swarm-server/test/bat-test.sh | 26 ++++++--- swarm-syncable/src/Client.js | 19 ++++--- swarm-syncable/src/OpStream.js | 13 +++++ 15 files changed, 170 insertions(+), 184 deletions(-) diff --git a/swarm-bat/src/StreamTest.js b/swarm-bat/src/StreamTest.js index 584ba09..6c403a9 100644 --- a/swarm-bat/src/StreamTest.js +++ b/swarm-bat/src/StreamTest.js @@ -75,7 +75,10 @@ class StreamTest { this.error = err; if (this.check_interval) clearInterval(this.check_interval); - this.streams.forEach(stream => stream.end()); + this.streams.forEach(stream => { + stream.end(); + stream.removeAllListeners(); + }); this.options.callback(err, this.results, this); } @@ -123,7 +126,8 @@ class StreamTest { if (this.output[stream_id]===undefined) this.output[stream_id] = ''; this.output[stream_id] += data.toString(); - this._check_output(false); + if (this.round) + this._check_output(false); }); } @@ -152,4 +156,4 @@ class StreamTest { StreamTest.SHORT_DELAY = 10; StreamTest.LONG_DELAY = 500; StreamTest.debug = false; -module.exports = StreamTest; \ No newline at end of file +module.exports = StreamTest; diff --git a/swarm-peer/src/PeerOpStream.js b/swarm-peer/src/PeerOpStream.js index 4e60efc..68d0e43 100644 --- a/swarm-peer/src/PeerOpStream.js +++ b/swarm-peer/src/PeerOpStream.js @@ -79,11 +79,13 @@ class PeerOpStream extends OpStream { this._emit(err); } - _processOn (op) { // FIXME LATE, NO EARLY!!!!!!!! + _processOn (op) { // FIXME REORDERING: LATE, NO EARLY!!!!!!!! let tip = this.tips.get(op.id); let top = this.vv.get(op.origin); + //FIXME NO CHANGES + if (op.Stamp.isZero()) this.queueScan(op); else if (tip>'0' && op.Stamp.value>tip) @@ -174,10 +176,8 @@ class PeerOpStream extends OpStream { // if (!this.met) // return this._emit(this.on_op.error('NO SUCH OP')); re_ops = ops.map(o => o.clearstamped(on.scope)).reverse(); - const max = re_ops.length ? re_ops[re_ops.length - 1].Stamp : Stamp.ZERO; - re_ops.push(on.stamped(max)); } else if (!ops.length) { // object unknown - re_ops = [on]; + re_ops = []; } else if (sync_fn) { // make a snapshot FIXME no state const last_op = ops[ops.length-1]; const state = last_op.method===Op.METHOD_STATE ? @@ -192,17 +192,21 @@ class PeerOpStream extends OpStream { this.db.replace(state, new_state, ()=>{}); else this.db.put(new_state, ()=>{}); // TODO? - re_ops = [new_state.scoped(on.scope), on.stamped(new_state.Stamp)]; + re_ops = [new_state.scoped(on.scope)]; } else { re_ops = ops.map(o => o.clearstamped(on.scope)).reverse(); - const max = re_ops[re_ops.length - 1].Stamp; - re_ops.push(on.stamped(max)); } this._emitAll(re_ops); + if (!err) { + let re_on = on; + if (on.isHandshake()) + re_on = on.stamped( this.vv.max ); + else if (re_ops.length) + re_on = on.stamped( re_ops[re_ops.length - 1].Stamp ); + this._emit(new Op(re_on, '')); + } } - - } PeerOpStream.VV_SPEC = new Spec('/VV#~!0.0'); diff --git a/swarm-peer/src/SwitchOpStream.js b/swarm-peer/src/SwitchOpStream.js index 4a0457b..dc43ccf 100644 --- a/swarm-peer/src/SwitchOpStream.js +++ b/swarm-peer/src/SwitchOpStream.js @@ -31,7 +31,7 @@ class SwitchOpStream extends OpStream { * @param {Function} callback */ constructor (db_repl_id, log, options, callback) { super(options); - this.dbrid = new Stamp(db_repl_id); + this._dbrid = new Stamp(db_repl_id); this.rid = null; this.log = log; this.subs = new Map(); @@ -45,8 +45,8 @@ class SwitchOpStream extends OpStream { this.meta = null; this.closing = new Map(); log.on(this); - const local_url = 'swarm://' + this.dbrid.origin + - '@local/' + this.dbrid; + const local_url = 'swarm://' + this.replicaId + + '@local/' + this.dbrid; // FIXME /dbid this.pocket = new Client(local_url, { upstream: this, debug: options.debug @@ -55,40 +55,35 @@ class SwitchOpStream extends OpStream { if (!err) { this.clock = this.pocket._clock; this.meta = this.pocket._meta; - this.rid = new ReplicaId(this.dbrid.origin, this.meta.replicaIdScheme); + this.rid = new ReplicaId(this.replicaId, this.meta.replicaIdScheme); // TODO to OpStream } callback && callback (err); }); - this.replid2connid.set (this.replicaId, '0'); - this.conns.set( '0', this.pocket ); - } - - get dbId () { - return this.dbrid.value; - } - - get replicaId () { - return this.dbrid.origin; } /*** * @param {OpStream} opstream - the client op stream */ on (opstream) { - // opstream._dbrid = null; - this.pending.push({ - stream: opstream, - hs: null, - rid: null, - ops: [], - client: null - }); + if (!this.pocket && opstream.constructor===Client) { + opstream._dbrid = this._dbrid; + this.replid2connid.set (this.replicaId, '0'); + this.conns.set( '0', opstream ); + } else { + this.pending.push({ + stream: opstream, + hs: null, + rid: null, + ops: [], + client: null + }); + } } off (opstream) { const dbrid = opstream._dbrid; if (!dbrid || this.repl2conn.get(dbrid)!==opstream) - return console.warn('unknown stream'); + return console.warn(new Error('unknown stream: '+dbrid).stack); const connid = this.repl2conn.get(dbrid); this.replid2connid.delete(dbrid.origin); this.conns.delete(connid); @@ -176,7 +171,7 @@ class SwitchOpStream extends OpStream { if (op===null) this._offer_end (null, stream); - else if (!stream._id) + else if (!stream._dbrid) this._offer_new (op, stream); else this._offer_op (op, stream); @@ -187,17 +182,17 @@ class SwitchOpStream extends OpStream { if (op.isState() && !op.Stamp.eq(op.Id)) { op = op.error('NO STATE PUSH', op.origin); } else if (Base64x64.isAbnormal(op.class) && stream!==this.pocket) { - op = op.error('PRIVATE CLASS', stream._id.origin); + op = op.error('PRIVATE CLASS', stream.replicaId); } else if (!Syncable.getClass(op.class)) { - op = op.error('CLASS UNKNOWN', stream._id.origin); + op = op.error('CLASS UNKNOWN', stream.replicaId); } else if (op.isOnOff()) { - if (stream._id.origin !== op.scope) - op = op.error('WRONG SCOPE', stream._id.origin); + if (stream.replicaId !== op.scope) + op = op.error('WRONG SCOPE', stream.replicaId); else if (op.isHandshake() && op.isOff()) - this.closing.set(stream._id, Date.now()); + this.closing.set(stream.replicaId, Date.now()); } else { - if (stream._id.origin !== op.origin) - op = op.error('WRONG ORIGIN', stream._id.origin); + if (stream.replicaId !== op.origin) + op = op.error('WRONG ORIGIN', stream.replicaId); } this.log.offer(op); } @@ -224,19 +219,19 @@ class SwitchOpStream extends OpStream { } _offer_end (_null, stream) { - if (!stream._id) { // purge silently + if (!stream._dbrid) { // purge silently let i = 0; const p = this.pending; while (iparseInt(d)); - if (!this.isCorrect()) - throw new Error('inconsistent replica id scheme formula'); this._offsets = [0, p[0], p[0]+p[1], p[0]+p[1]+p[2]]; } @@ -66,14 +64,13 @@ class ReplicaIdScheme { } static is (scheme) { - if (!ReplicaIdScheme.FORMAT_RE.test(scheme)) return false; const rids = new ReplicaIdScheme(scheme); return rids.isCorrect(); } isCorrect () { const length = this.primuses+this.peers+this.clients+this.sessions; - return length<=10; + return length<=10 && length>0; } isAbnormalPart (part, i) { @@ -110,4 +107,3 @@ ReplicaIdScheme.DEFAULT_SCHEME = '0262'; module.exports = ReplicaIdScheme; - diff --git a/swarm-protocol/src/VV.js b/swarm-protocol/src/VV.js index c9f2f02..c0635a0 100644 --- a/swarm-protocol/src/VV.js +++ b/swarm-protocol/src/VV.js @@ -6,11 +6,12 @@ class VV { constructor (vec) { this.map = new Map(); + this._max = '0'; if (vec) { this.addAll(vec); } } - + // simple string serialization of the vector toString () { var stamps = []; @@ -35,11 +36,14 @@ class VV { if (value>existing && value!=='0') { this.map.set(origin, value); } + if (value > this._max) + this._max = value; } - remove (origin) { + remove (origin) { // FIXME remove this op + console.warn('VV.remove() is deprecated'); origin = VV.norm_src(origin); - delete this.map.delete(origin); + this.map.delete(origin); return this; } @@ -92,12 +96,7 @@ class VV { } get max () { - var max = '0'; - for (var ts of this.map.values()) { - if (ts>max) - max = ts; - } - return max; + return this._max; } static norm_src (origin) { diff --git a/swarm-server/src/NodeServerOpStream.js b/swarm-server/src/NodeServerOpStream.js index a4f5e92..b28048e 100644 --- a/swarm-server/src/NodeServerOpStream.js +++ b/swarm-server/src/NodeServerOpStream.js @@ -85,7 +85,8 @@ class NodeServerOpStream extends OpStream { console.warn('<'+this._debug+'\t['+(end?(ops.length-1)+'EOF':ops.length)+']'); if (end) { ops.pop(); - this._stream.end(Op.serializeFrame(ops)); + this._stream.write(Op.serializeFrame(ops)); + this._stream.end(); this._stream = null; } else { this._stream.write(Op.serializeFrame(ops)); diff --git a/swarm-server/src/swarm b/swarm-server/src/swarm index 6051e1c..08c40ff 100755 --- a/swarm-server/src/swarm +++ b/swarm-server/src/swarm @@ -12,82 +12,3 @@ cli .command("user [options] ", "create/edit users").alias('U') .command("run [options] ", "run a database (the default)", {isDefault: true}).alias('R') .parse(process.argv); - -/* -var help = [ - '', - 'Basic Swarm server. Usage: ', - ' swarm [-C|-F|-A|-R] path/to/dbid-rid [options]', - '', - '-C --create create a database (dir name == db name)', - ' --oXxx="Yyy" set a global database option Xxx to "Yyy"', - ' --OXxx="Yyy" set a scoped database option', - '-F --fork fork a database', - ' -t --to /path/dbname-YZ a path for the new replica', - ' -i --id YZ new replica id', - '-A --access , access a database', - ' -s --scan /Type#id!prefix list all records under a prefix', - ' -e --erase /Type#id!prefix erase records', - ' -p --put filename add ops to the database', - ' -v --vv print the version vector', - " -g --get /Type#id print the object's state (merge ops)", - ' --OXxx, --0Xxx edit database options (as above)', - '-R --run run a database (the default)', - ' -l --listen scheme:url listen for client conns on URL', - ' (WebSocket ws://host:port, TCP tcp:...)', - ' -c --connect scheme:url connect to a peer', - ' -e --exec script.js execute a script once connected (default: REPL)', - ' -d --daemon daemonize', - ' -i --ingest file.op ingest ops from a file (default: stdin/out)', - ' -f --filter grep log events (e.g. -f /Swarm.on.off)', - ' -a --auth auth OpStream implementation (default: trusty)', - '-U --user add/remove/list users/clients', - ' -a --add login add a user (take the password from stdin)', - ' -r --remove login', - ' -l --list', - '', - '-T --trace [SLP] trace op processing pipeline (switch/log/patch)', - '' -].join('\n'); - -if (args.h || args.help) { - console.log(help); - process.exit(0); -} - -let create = args.C || args.create; -let fork = args.F || args.fork; -let access = args.A || args.access; -let run = args.R || args.run; -let user = args.U || args.user; - -if (create) { - require('./src/create')(create, args, done); -} else if (fork) { - require('./src/fork')(fork, args, done); -} else if (access) { - require('./src/access')(access, args, done); -} else if (run) { - require('./src/run')(run, args, done); -} else if (user) { - require('./src/user')(user, args, done); -} else { - console.log(help); - done('no run mode specified'); -} - -function done (err) { - if (err) { - if (args.v) { - console.error(new Error(err).stack); - } else { - console.error(err); - } - } - process.exit(err?-1:0); -} - -process.on('uncaughtException', function (err) { - console.error("UNCAUGHT EXCEPTION", err, err.stack); -}); -*/ diff --git a/swarm-server/src/swarm-create b/swarm-server/src/swarm-create index 32c8dc3..81c2370 100755 --- a/swarm-server/src/swarm-create +++ b/swarm-server/src/swarm-create @@ -5,12 +5,14 @@ const path = require('path'); const cli = require("commander"); const swarm = require('swarm-protocol'); const sync = require('swarm-syncable'); +const peer = require('swarm-peer'); +const LevelDOWN = require('leveldown'); require('./api'); const Base64x64 = swarm.Base64x64; -// const Swarm = sync.Swarm; +const Swarm = sync.Swarm; const Stamp = swarm.Stamp; const Clock = swarm.Clock; -// const Op = swarm.Op; +const Op = swarm.Op; const ReplicaIdScheme = swarm.ReplicaIdScheme; function more_options (kv, opts) { @@ -27,7 +29,7 @@ cli .usage("[options] ") .option("-c, --clocklen ", "Timestamp minimum length", parseInt) .option("-l, --logical", "Use sequential (not hybrid) timestamps") - .option("-s, --scheme", "Replica id scheme", /^\d{3,4}$/) + .option("-s, --scheme ", "Replica id scheme", /^\d{3,4}$/) .option("-d, --debug", "trace ops") .option("-o, --option ", "Database option", more_options, {}) .parse(process.argv); @@ -61,21 +63,51 @@ if (replid.length!==scheme.peers+scheme.primuses) { return 4; } -const host = new sync.Client( - 'swarm+level:'+db_path, - {debug: !!cli.debug}, - create_meta -); +const options = Object.create(null); +if (cli.logical) + options[Clock.OPTION_CLOCK_MODE] = "Logical"; +if (cli.clocklen) + options[Clock.OPTION_CLOCK_LENGTH] = cli.clocklen; +options[ReplicaIdScheme.DB_OPTION_NAME] = scheme.toString(); +const clock = new swarm.Clock(replid, options); -function create_meta (err) { - const meta = host.meta; - meta.set(ReplicaIdScheme.DB_OPTION_NAME, scheme.toString()); - if (cli.clocklen) - meta.set(Clock.OPTION_CLOCK_LENGTH, cli.clocklen); - if (cli.logical) - meta.set(Clock.OPTION_CLOCK_MODE, "Logical"); - meta.setAll(cli.option); - //host.onSync(90=>console.log('database created')); +const level = new peer.LevelOp(new LevelDOWN(db_path), { + createIfMissing: true, + errorIfExists: true +}, write_ops); + +function write_ops () { + const ops = []; + cli.logical && ops.push(new Op([ + Swarm.RDT.Class, + dbid, + clock.time(), + Clock.OPTION_CLOCK_MODE], + '"Logical"')); + cli.clocklen && ops.push(new Op([ + Swarm.RDT.Class, + dbid, + clock.time(), + Clock.OPTION_CLOCK_LENGTH], + cli.clocklen)); + ops.push(new Op([ + Swarm.RDT.Class, + dbid, + clock.time(), + ReplicaIdScheme.DB_OPTION_NAME], + '"'+scheme.toString()+'"')); + level.putAll(ops, err => { + if (err) + console.error(err); + else + level.close(done); + }); + +} + +function noop() {} + +function done () { console.log('created database', dbid, 'replica', replid); } diff --git a/swarm-server/src/swarm-db b/swarm-server/src/swarm-db index 108b5ec..d779e20 100755 --- a/swarm-server/src/swarm-db +++ b/swarm-server/src/swarm-db @@ -25,7 +25,7 @@ if (!cli.args[0]) { } const db_path = path.resolve(cli.args[0]); if (!fs.existsSync(db_path) || !fs.statSync(db_path).isDirectory()) { - console.warn('the dir does not exist'); + console.warn('the dir does not exist: '+db_path); return 2; } diff --git a/swarm-server/src/swarm-run b/swarm-server/src/swarm-run index cf78b0c..5fcb199 100755 --- a/swarm-server/src/swarm-run +++ b/swarm-server/src/swarm-run @@ -99,6 +99,10 @@ function on_err (err) { process.exitCode = 1; } +process.on('uncaughtException', function (err) { + console.error("UNCAUGHT EXCEPTION", err, err.stack); +}); + // process.on('SIGTERM', on_err.bind('SIGTERM')); TODO graceful close // process.on('SIGINT', on_err.bind('SIGINT')); // process.on('SIGQUIT', on_err.bind('SIGQUIT')); diff --git a/swarm-server/src/swarm-user b/swarm-server/src/swarm-user index 0c37451..232442c 100755 --- a/swarm-server/src/swarm-user +++ b/swarm-server/src/swarm-user @@ -58,20 +58,21 @@ if (!cli.args[0]) { } const db_path = path.resolve(cli.args[0]); if (!fs.existsSync(db_path) || !fs.statSync(db_path).isDirectory()) { - console.warn('the dir does not exist'); + console.warn('the dir does not exist: '+db_path); return 2; } let userid = cli.id; let user; +const opts = {debug: !!cli.debug}; if (!Base64x64.is(userid)) return on_err('user id must be Base64x64'); // FIXME probably, generate id -const host = new sync.Client('swarm+level:'+db_path, {debug: true}, err => { +const host = new sync.Client('swarm+level:'+db_path, opts, err => { if (err) return on_err(err); diff --git a/swarm-server/test/bat-test.sh b/swarm-server/test/bat-test.sh index 878b5a4..a62a6e2 100755 --- a/swarm-server/test/bat-test.sh +++ b/swarm-server/test/bat-test.sh @@ -3,21 +3,29 @@ NODE=`which node` echo $NODE && $NODE -v cd "$( dirname "${BASH_SOURCE[0]}" )" TEST_DIR=../../protocol-docs/test -SWARM="$NODE ../cli.js" +SWARM="$NODE ../src/swarm" #BAT=bat BAT="$NODE ../../swarm-bat/bat-cli.js" #if ! which bat; then # BAT=../../swarm-bat/bat-cli.js #fi -DB=test-R +DB=./test+R rm -rf $DB echo CREATE DB -$SWARM -C $DB \ - --oDBIdScheme 172 \ - --oClock "Logical" \ - --oClockLen 5 -$SWARM -A $DB -s -echo 1 | $SWARM -U $DB -a testusr + +$SWARM create \ + --scheme 172 \ + --logical \ + --clocklen 5 \ + $DB \ + || exit 1 + +echo SCAN +$SWARM db $DB || exit 2 + +echo CREATE USER +$SWARM user --id 0testusr --password 1 $DB || exit 3 + echo BASIC PEER TESTS -$BAT -e "$SWARM -R $DB -l" $TEST_DIR/peer-basic.batt || exit $? +$BAT -e "$SWARM run -D -l std: $DB" $TEST_DIR/peer-basic.batt || exit $? diff --git a/swarm-syncable/src/Client.js b/swarm-syncable/src/Client.js index a6e4b12..f7db632 100644 --- a/swarm-syncable/src/Client.js +++ b/swarm-syncable/src/Client.js @@ -46,7 +46,8 @@ class Client extends OpStream { this._syncables = Object.create(null); /** we can only init the clock once we have a meta object */ this._clock = null; - this._last_acked = Stamp.ZERO; + this._ssn_stamp = this._last_stamp = this._acked_stamp = Stamp.ZERO; + this._unsynced = new Map(); this._upstream = this.options.upstream; if (!this._upstream) { let next = this._url.clone(); @@ -55,8 +56,6 @@ class Client extends OpStream { this._upstream = OpStream.connect(next, options); } this._upstream.on(this); - this._unsynced = new Map(); - this._ssn_stamp = this._last_stamp = this._acked_stamp = Stamp.ZERO; this._meta = this.get( SwarmMeta.RDT.Class, this.dbid, @@ -64,13 +63,9 @@ class Client extends OpStream { this._clock = new swarm.Clock(state.scope, this._meta.filterByPrefix('Clock')); this._id = new Stamp(state.birth, state.scope); this._clock.seeTimestamp(state.Stamp); - this._ssn_stamp = this._last_stamp = this._acked_stamp = this.time(); callback && callback(); } ); - this._meta.onceSync ( - reon => this._clock.seeTimestamp(reon.Stamp) - ); if (!Syncable.defaultHost) // TODO deprecate Syncable.defaultHost = this; } @@ -97,12 +92,22 @@ class Client extends OpStream { const syncable = this._syncables[op.object]; if (!syncable) return; const rdt = syncable._rdt; + if (!op.Stamp.isAbnormal() && this._clock) this._clock.seeTimestamp(op.Stamp); + + if (this._ssn_stamp.isZero() && this._clock && op.isOn()) { //FIXME + this._ssn_stamp = this._acked_stamp = this.time(); + if (this._last_stamp.isZero()) + this._last_stamp = this._ssn_stamp; + } + if (op.isOnOff()) this._unsynced.delete(op.object); // FIXME to RDT + if (!op.isOnOff() && op.origin===this.origin && op.Stamp.gt(this._ssn_stamp)) { // :( this._acked_stamp = op.Stamp; + console.warn('ACKED: '+op.stamp+' '+this._ssn_stamp) } else { if (!rdt && op.name !== "off") this.offer(new Op(op.renamed('off', this.replicaId), ''), this); diff --git a/swarm-syncable/src/OpStream.js b/swarm-syncable/src/OpStream.js index 660e3ca..50bf598 100644 --- a/swarm-syncable/src/OpStream.js +++ b/swarm-syncable/src/OpStream.js @@ -20,6 +20,18 @@ class OpStream { this.error_message = null; } + get replicaId () { + return this._dbrid && this._dbrid.origin; + } + + get dbId () { + return this._dbrid && this._dbrid.value; + } + + get dbrid () { + return this._dbrid.toString(); + } + _lstn_state () { if (this._lstn===null) return MUTE; @@ -135,6 +147,7 @@ class OpStream { this._lstn.push(op); break; } + // this._emit(null) removes all listeners to prevent further emits if (op===null) this._lstn = null; } From d506f3017876c431bd8fa3a76c9f126a8d0fe8db Mon Sep 17 00:00:00 2001 From: Victor Grishchenko Date: Tue, 25 Oct 2016 12:42:00 +0500 Subject: [PATCH 28/51] make sure .on can not outrace ops --- swarm-peer/src/PeerOpStream.js | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/swarm-peer/src/PeerOpStream.js b/swarm-peer/src/PeerOpStream.js index 68d0e43..15ce00a 100644 --- a/swarm-peer/src/PeerOpStream.js +++ b/swarm-peer/src/PeerOpStream.js @@ -32,8 +32,8 @@ class PeerOpStream extends OpStream { if (callback) callback(err, this); }); - this.offered_queue = []; this.save_queue = []; + this.scan_queue = []; this.pending_scans = []; this.active_scans = []; @@ -65,12 +65,15 @@ class PeerOpStream extends OpStream { _save_batch () { let save = this.save_queue; this.save_queue = []; + const scans = this.scan_queue; + this.scan_queue = []; this.db.putAll(save, err => { this._handle = null; this._emitAll(save.map( op => op.clearstamped() )); if (this.save_queue.length) this._handle = setImmediate(this._save_cb); + scans.forEach( on => this.queueScan(on) ); }); } @@ -87,22 +90,22 @@ class PeerOpStream extends OpStream { //FIXME NO CHANGES if (op.Stamp.isZero()) - this.queueScan(op); + this.scan_queue.push(op); else if (tip>'0' && op.Stamp.value>tip) this._emit(op.error('UNKNOWN BASE > '+tip)); else if (!top || top '+top)); // leaks max stamp else - this.queueScan(op); + this.scan_queue.push(op); } _processOff (off) { - if (off.spec.class===sync.Swarm.id) { + if (off.spec.clazz===sync.Swarm.RDT.Class) { const origin = off.scope; let top = this.vv.get(origin); - off = off.restamped(top); + off = off.stamped(top); } this._emit(off); // FIXME order same-source same-object @@ -136,15 +139,6 @@ class PeerOpStream extends OpStream { this._processMutation(state_op, this.save, this.forward); } - _apply (op, source) { - if (op===null) { - const i = this.active_scans.indexOf(source); - this.active_scans.splice(i, 1); - this.queueScan(); - } else { - this._emit(op); - } - } queueScan (on) { if (on) From f4f46c66cd50b851732bd859cb4f95587bcab530 Mon Sep 17 00:00:00 2001 From: Victor Grishchenko Date: Thu, 3 Nov 2016 10:18:15 +0500 Subject: [PATCH 29/51] swarm-cli first session open reworked Syncable callbacks, added NodeOpStreams --- swarm-cli/cli.js | 184 ----------------------- swarm-cli/src/NodeOpStream.js | 40 +++-- swarm-cli/src/swarm-client | 195 +++++++++++++++++++++++++ swarm-peer/src/SwitchOpStream.js | 16 +- swarm-protocol/src/Op.js | 9 ++ swarm-server/src/NodeServerOpStream.js | 21 ++- swarm-syncable/index.js | 2 + swarm-syncable/src/Client.js | 28 +++- swarm-syncable/src/Syncable.js | 55 +++++-- 9 files changed, 325 insertions(+), 225 deletions(-) delete mode 100755 swarm-cli/cli.js create mode 100755 swarm-cli/src/swarm-client diff --git a/swarm-cli/cli.js b/swarm-cli/cli.js deleted file mode 100755 index c91ed48..0000000 --- a/swarm-cli/cli.js +++ /dev/null @@ -1,184 +0,0 @@ -#!/usr/bin/env node -"use strict"; -const fs = require('fs'); -const async = require('async'); -const swarm = require('swarm-protocol'); -const cli = require('commander'); -const Stamp = swarm.Stamp; -const Spec = swarm.Spec; - -const idre = new RegExp('^[/#]?(T)(?:[#/\\](T))?'.replace(/T/g, Stamp.rsTokExt)); - -function parseId (arg, list) { // TODO stdin / file read!!! - list = list || []; - const m = idre.exec(arg); // TODO id list, comma/space separated - if (m===null) { - console.warn('invalid object id', arg); - } else { - const spec = new Spec(m[2] ? [m[1], m[2]] : ['LWWObject', m[1]] ); - list.push(spec); - } - return list; -} - -cli - .version('0.0.1') - .arguments('[path]') - .option("-C --connect ", 'connect to a server, init a replica') - .option("-u --update [object]", "update an object (e.g. -u 1GLCR5+Rgritzko1)", parseId) - .option("-c --create [type]", "create an object (e.g. -c LWWObject)", "LWWObject") - .option("-g --get ", "retrieve the current version of the object", parseId) - .option("-c --cat ", "print out the object's JSON", parseId) - .option("-r --recur ", "recursive retrieval", parseInt) - .option("-p --put ", "commit a manually edited JSON object", parseId) - .option("-e --edit ", "edit a JSON state, put when done (uses $EDITOR)", parseId) - .option("-o --op ", "feed an op", parseId) - .option(" -n --name ", "op name to feed") - .option(" -v --value ", "op value to feed") - .option("-E --exec ", "execute a script (e.g. --exec init.js -E run.js)") - .option("-R --repl", "run REPL") - .option("-L --log", "list the log of yet-unacked local ops") - .option("-m --mute", "don't talk to the server") - .option("-n --now", "issue a timestamp") - .option("-v --verbose", "verbosity level") - .parse(process.argv) -; - - -let client, cache, upstream; - -const stages = [ - do_connect, - do_update, - do_create, - do_get, - do_cat, - do_put, - do_edit, - do_op, - do_sync -]; - - -async.waterfall( stages, done ); - - -function json_file_name (spec) { - cache.filePath(spec); -} - -function do_connect (next) { - cli.connect; - client = new Client(); - cache = new node.FilesystemCache(client); - upstream = new node.StdioOpStream(); // TODO upstream.retry(), backoff -} - -function do_update (next) { - const list = cli.update; - if (!list || !list.length) return next(); - let inc = list.length, dec = 0; - while (list.length) - client.get(list.pop(), () => ++dec===inc && next() ); -} - -function do_create (next) { - const list = cli.create; - if (!list || !list.length) return next(); - try { - list.forEach( type => client.create(type) ); - next(); - } catch (ex) { - next(ex.message); - } -} - -function do_get (next) { - const list = cli.get; - if (!list || !list.length) return next(); - let inc = list.length, dec = 0; - while (list.length) { - client.get(list.pop(), (obj) => { - console.log(obj.object); - ++dec === inc && next(); - }); - } -} - -function do_cat (next) { - const list = cli.cat; - if (!list || !list.length) return next(); - list.forEach( id => client.get(id, - obj => console.log(obj.object, obj.toJSON(4)) - ) ); - next(); -} - -function do_put (next) { // GOOOOD - const list = cli.put; - if (!list || !list.length) return next(); - const files = list.map(json_file_name); - if (!files.every(fs.existsSync)) - return next('file not found'); - const jsons = files.map( fs.readFileSync ); - const parseds = jsons.map( JSON.parse ); - list.map( (spec, i) => client.get(spec, obj => obj.save(parseds[i])) ); - next(); -} - -function do_edit (next) { - const list = cli.edit; - if (!list || !list.length) return next(); - const child = require('child_process'); - const editor = process.env.EDITOR; - if (!editor) return next('$EDITOR not defined'); - - cli.get = list; - do_get ( err => client.onSync(edit_all) ); - - function edit_all() { - const files = list.map(json_file_name); - files.forEach( file => child.execFileSync(editor, file) ); - cli.put = list; - do_put(next); - } - -} - -function do_op (next) { - const list = cli.edit; - if (!list || !list.length) return next(); - const id = list.shift(); - const name = cli.name; - const value = cli.value; - if (!name || !value) { - next('need a name/value pair'); - } else { - client.submit(id, cli.name, cli.value); - next(); - } -} - -function do_sync (next) { - if (cli.mute) { - next(); - } else { - client.onSync(next); - } -} - -function done (err) { - if (err) { - if (cli.verbose) { - console.error(new Error(err).stack); - } else { - console.error(err); - } - } - process.exit(err?-1:0); -} - -// be ready -process.on('uncaughtException', function (err) { - console.error("UNCAUGHT EXCEPTION", err, err.stack); -}); diff --git a/swarm-cli/src/NodeOpStream.js b/swarm-cli/src/NodeOpStream.js index 8ddd7cb..f1356c0 100644 --- a/swarm-cli/src/NodeOpStream.js +++ b/swarm-cli/src/NodeOpStream.js @@ -1,4 +1,5 @@ "use strict"; +const net = require('net'); const swarm = require('swarm-protocol'); const sync = require('swarm-syncable'); const OpStream = sync.OpStream; @@ -10,10 +11,20 @@ const Op = swarm.Op; class NodeOpStream extends OpStream { /** @param stream - Node.js stream */ - constructor (stream) { - super(); - this._stream = stream; - stream.setEncoding('utf8'); + constructor (url, options) { + super(options); + const scheme = url.scheme[0]; + if (scheme==='std') { + this._stream = require('duplexify')(process.stdout, process.stdin); + this._stream.end = function(){console.warn('end');}; + } else if (scheme==='tcp') { + this._stream = net.connect(url.port||31415, url.hostname, options); + } else if (scheme==='sock') { + this._stream = net.connect(url.path, options); + } else { + throw new Error('protocol unknown: '+scheme); + } + this._stream.setEncoding('utf8'); this._chunks = []; this._ops = []; this._send_to = null; @@ -36,7 +47,7 @@ class NodeOpStream extends OpStream { this._on_batch(); this._on_data(chunk.substr(1)); } else if (at!==-1) { - chunks.push(chunk.substr(0, at+2)) + chunks.push(chunk.substr(0, at+2)); this._on_batch(); if (at+2 \t['+ops.length+']'); let frame = Op.serializeFrame(ops); this._stream.write(frame); this._ops.length = 0; @@ -88,17 +103,22 @@ class NodeOpStream extends OpStream { this.close(); } + // FIXME rework close/null/end sequences close () { if (this._stream===null) return; this._stream.removeListener('data', this._on_data_cb); this._stream.removeListener('end', this._on_end_cb); this._send(); - this._stream.end(); - this._stream = null; - super._end(); + // this._stream.end(); + // this._stream = null; + // super._end(); } } -module.exports = NodeOpStream; \ No newline at end of file +OpStream._URL_HANDLERS.tcp = NodeOpStream; +OpStream._URL_HANDLERS.std = NodeOpStream; +OpStream._URL_HANDLERS.sock = NodeOpStream; + +module.exports = NodeOpStream; diff --git a/swarm-cli/src/swarm-client b/swarm-cli/src/swarm-client new file mode 100755 index 0000000..4ad5e68 --- /dev/null +++ b/swarm-cli/src/swarm-client @@ -0,0 +1,195 @@ +#!/usr/bin/env node +"use strict"; +const fs = require('fs'); +const path = require('path'); +const child = require('child_process'); +const swarm = require('swarm-protocol'); +const sync = require('swarm-syncable'); +const swarm_cli = require('..'); +const cli = require('commander'); +const Stamp = swarm.Stamp; +const Spec = swarm.Spec; + +cli + .version('0.0.1') + .arguments('[path]') + .option("-C --connect ", 'connect to a server, init a replica', + str=>new sync.URL(str)) + .option("-c --create [type]", "create an object (e.g. -c LWWObject)") + .option("-g --get ", "retrieve the object", arg2specs) + .option("-r --recur ", "recursive retrieval", parseInt) + .option("-p --put ", "commit a manually edited JSON object", arg2specs) + .option("-e --edit ", "edit and put a JSON state(uses $EDITOR)", arg2specs) + .option("-o --op ", "feed an op", arg2specs) + .option(" -n --name ", "op name to feed") + .option(" -v --value ", "op value to feed") + .option("-E --exec ", "execute a script") + .option("-R --repl", "run REPL") + .option("-L --log", "list the log of yet-unacked local ops") + .option("-m --mute", "don't talk to the server") + .option("-n --now", "issue a timestamp") + .option("-8 --stay", "run indefinitely") + .option("-D --debug", "tracing") + .parse(process.argv) +; + +const re_path = new RegExp( + "(B)/+(B)(\\.\\w+)?$".replace(/B/g, swarm.Base64x64.rs64x64) +); + +// takes filenames, ids or specs, produces [spec] +function arg2specs (arg, list) { // TODO stdin / file read!!! + list = list || []; + if (Stamp.is(arg)) { + list.push(new Spec([ + sync.LWWObject.RDT.Class, + new Stamp(arg), + Stamp.ZERO, Stamp.ZERO + ])); + } else if (Spec.is(arg)) { + list.push(new Spec(arg)); + } else if (re_path.test(arg)) { // path? + re_path.lastIndex = 0; + const m = re_path.exec(arg); + list.push(new Spec([ + new Stamp(m[1]), + new Stamp(m[2]), + Stamp.ZERO, Stamp.ZERO + ])); + } else { + console.warn('malformed spec: '+arg); + } + return list; +} + +function spec2path (spec) { + return cache.filePath(spec); +} + +let dir; +if (cli.args[0]) { + dir = cli.args[0]; +} else if (fs.existsSync('.swarm')) { + dir = '.'; +} else if (cli.connect) { + dir = path.basename(cli.connect.path); +} else { + console.error('no dir specified'); + return 1; +} + +const db_path = path.resolve(dir); +const config_path = path.join(db_path, '.swarm'); + +if (!fs.existsSync(db_path)) { + if (!cli.connect) { + console.error('no such dir'); + return 3; + } + fs.mkdirSync(db_path); + const json = JSON.stringify({connect: cli.connect.toString()}); + fs.writeFileSync(config_path, json); +} else if (!fs.statSync(db_path).isDirectory()) { + console.error('not a dir'); + return 2; +} else if (!fs.existsSync(config_path)) { + console.error('not a swarm dir: '+db_path); + return 4; +} + + +const config = fs.readFileSync(config_path); +const props = JSON.parse(config); +const connect = new sync.URL + (cli.connect || props.connect || 'swarm+fs+tcp://localhost/test'); +const options = { debug: !!cli.debug }; + +const client = new sync.Client(connect, options, err => { + if (err) { + console.error(err); + } else { + do_things(); + } +}); +const cache = client.upstream; + + +function do_things (op) { + + if (cli.now) + console.log(client.time().toString()); + + // console.warn(cli); + + // TODO .get options {recur: 5} + // TODO default: update all objects + cli.get && cli.get.forEach( spec => + client.get(spec, () => cache.flush(spec, true)) + ); + + if (cli.create) { + const clazz = cli.create===true ? 'LWWObject' : cli.create; + console.log ( client.create(clazz)._id ); + } + + cli.cat && cli.cat.forEach( spec => + client.get(spec, obj => console.log(obj.toString()) ) + ); + + cli.put && cli.put.forEach ( spec => + client.get(spec, obj => + obj.fromString(fs.readFileSync(spec2path(spec))) + ) + ); + + const editor = process.env.EDITOR || 'vi'; + cli.put && cli.put.forEach ( spec => + client.get(spec, obj => { + const path = spec2path(spec); + cache.flush(spec, true); + child.execFileSync(editor, path); + obj.fromString(fs.readFileSync(path)); + }) + ); + + cli.op && cli.op.forEach ( spec => + client.get(spec, obj => + obj._offer (cli.name||'0', cli.value||'') + ) + ); + + if (cli.exec) + require(cli.exec); + + if (cli.mute) { + done(); + } else if (cli.stay) { + + } else if (cli.repl) { + require('repl').start({ + prompt:'> ', + useColors: true, + useGlobal: true + }); + } else { + client.onSync(done); + } + +} + + +function done (err) { + if (err) { + if (cli.debug) { + console.error(new Error(err).stack); + } else { + console.error(err); + } + } + process.exit(err?-1:0); +} + +// be ready +process.on('uncaughtException', function (err) { + console.error("UNCAUGHT EXCEPTION", err, err.stack); +}); diff --git a/swarm-peer/src/SwitchOpStream.js b/swarm-peer/src/SwitchOpStream.js index dc43ccf..7fa089c 100644 --- a/swarm-peer/src/SwitchOpStream.js +++ b/swarm-peer/src/SwitchOpStream.js @@ -201,7 +201,7 @@ class SwitchOpStream extends OpStream { const req = this.req4stream(stream); if (!req) throw new Error('unknown stream'); - if (!req.hs && op.isHandshake() && op.scope) { + if (!req.hs && op.isHandshake() && op.scope!=='0') { req.hs = op; req.rid = new ReplicaId(op.scope, this.meta.replicaIdScheme); if (req.stream===this.pocket) @@ -211,6 +211,9 @@ class SwitchOpStream extends OpStream { req.rid.client, this._auth_client.bind(this, req) ); + } else if (!req.hs && op.isHandshake()) { + req.hs = op; + this._deny (req, 'INVALID HANDSHAKE'); } else if (!req.hs) { this._deny (req, 'HANDSHAKE FIRST'); } else { @@ -239,7 +242,10 @@ class SwitchOpStream extends OpStream { _deny (hs_obj, message) { if (hs_obj.hs) - hs_obj.stream._apply(hs_obj.hs.error(message)); + hs_obj.stream._apply(hs_obj.hs.named(Op.METHOD_OFF, message)); + else if (message) + hs_obj.stream._apply( + new Op([sync.Swarm.RDT.Class,'0','0',Op.METHOD_OFF], message)); hs_obj.stream._apply(null); } @@ -295,14 +301,14 @@ class SwitchOpStream extends OpStream { if (creds && creds[0]==='{') props = JSON.parse(creds); // FIXME report err else - props = {password: creds}; + props = {Password: creds}; } catch (ex) {} if (!props || !client.hasState()) { this._deny(req, 'INVALID CREDENTIALS 1'); - } else if (props.password===client.get('password')) { + } else if (props.password===client.get('Password')) { this._accept(req); } else { - console.warn(props, props.password,client.get('password')); + console.warn(props, props.password,client.get('Password')); this._deny(req, 'INVALID CREDENTIALS 2'); } const i = this.pending.indexOf(req); diff --git a/swarm-protocol/src/Op.js b/swarm-protocol/src/Op.js index b2290d3..9b200cb 100644 --- a/swarm-protocol/src/Op.js +++ b/swarm-protocol/src/Op.js @@ -170,6 +170,15 @@ class Op extends Spec { ], this._value); } + named (name, value) { + return new Op([ + this.Type, + this.Id, + this.Stamp, + name + ], value || this._value); + } + static zeroStateOp (spec) { return new Op([spec.Type, spec.Id, Stamp.ZERO, Op.METHOD_STATE], ''); } diff --git a/swarm-server/src/NodeServerOpStream.js b/swarm-server/src/NodeServerOpStream.js index b28048e..35e3b40 100644 --- a/swarm-server/src/NodeServerOpStream.js +++ b/swarm-server/src/NodeServerOpStream.js @@ -52,16 +52,23 @@ class NodeServerOpStream extends OpStream { this._chunks = []; let ops = swarm.Op.parseFrame(frame); if (!ops.length || ops===null) { - this._close(); - } else { + this._close('parsing error'); + } else try { this.offerAll(ops); + } catch (ex) { + console.error(ex); + this._close('internal error: '+ex.message); } } offer (op) { + if (this._upstream===null) + return; if (this._debug) - console.warn(this._debug+'}}\t'+(op?op.toString():'[EOF]')); + console.warn(this._debug+'}\t'+(op?op.toString():'[EOF]')); this._upstream.offer(op, this); + if (op===null) + this._upstream = null; } _apply (op) { @@ -93,6 +100,14 @@ class NodeServerOpStream extends OpStream { } } + _close (msg) { + if (msg) { + this._apply(new Op(['0','0','0',Op.METHOD_OFF], msg)); + this._apply(null); + } + this.offer(null); + } + _on_end () { if (this._stream===null) return; // closed diff --git a/swarm-syncable/index.js b/swarm-syncable/index.js index 354ec93..6807148 100644 --- a/swarm-syncable/index.js +++ b/swarm-syncable/index.js @@ -2,10 +2,12 @@ var Swarm = { Host: require('./src/Client'), Client: require('./src/Client'), + Cache: require('./src/Cache'), Syncable: require('./src/Syncable'), OpStream: require('./src/OpStream'), LWWObject: require('./src/LWWObject'), Swarm: require('./src/Swarm'), + URL: require('./src/URL'), get: get_fn }; diff --git a/swarm-syncable/src/Client.js b/swarm-syncable/src/Client.js index f7db632..9610de5 100644 --- a/swarm-syncable/src/Client.js +++ b/swarm-syncable/src/Client.js @@ -52,14 +52,19 @@ class Client extends OpStream { if (!this._upstream) { let next = this._url.clone(); next.scheme.shift(); - if (!next.scheme.length) throw new Error('upstream not specified'); + if (!next.scheme.length) + throw new Error('upstream not specified'); this._upstream = OpStream.connect(next, options); } this._upstream.on(this); this._meta = this.get( SwarmMeta.RDT.Class, this.dbid, - state => { // FIXME htis must be state!!! + (err, meta, state) => { + if (err) { + callback && callback(err); + return; + } this._clock = new swarm.Clock(state.scope, this._meta.filterByPrefix('Clock')); this._id = new Stamp(state.birth, state.scope); this._clock.seeTimestamp(state.Stamp); @@ -107,7 +112,6 @@ class Client extends OpStream { if (!op.isOnOff() && op.origin===this.origin && op.Stamp.gt(this._ssn_stamp)) { // :( this._acked_stamp = op.Stamp; - console.warn('ACKED: '+op.stamp+' '+this._ssn_stamp) } else { if (!rdt && op.name !== "off") this.offer(new Op(op.renamed('off', this.replicaId), ''), this); @@ -185,7 +189,7 @@ class Client extends OpStream { fetch (spec, on_state) { const have = this._syncables[spec.object]; if (have) { - on_state && on_state(); + on_state && setImmediate(on_state); // force async return have; } const state0 = new Op( [spec.Type, spec.Id, Stamp.ZERO, Op.STAMP_STATE], '' ); @@ -193,9 +197,9 @@ class Client extends OpStream { if (!fn) throw new Error('unknown syncable type '+spec); const rdt = new fn.RDT(state0, this); - const on = rdt.toOnOff(true).scoped(this._id.origin); - if (on.isHandshake() && this._url.password) - on._value = JSON.stringify({Password: this._url.password});// FIXME + let on = rdt.toOnOff(true).scoped(this._id.origin); // FIXME scope + if (on.isHandshake()) + on = this._populate_handshake(on); const syncable = new fn(rdt, on_state); this._syncables[spec.object] = syncable; this._unsynced.set(spec.object, 1); @@ -203,6 +207,14 @@ class Client extends OpStream { return syncable; } + _populate_handshake (on) { + if (this._url.replica) + on = on.scoped(this._url.replica); + if (this._url.password) + on._value = JSON.stringify({Password: this._url.password}); + return on; + } + _remove_syncable (obj) { let prev = this._syncables[obj.typeid]; if (prev!==obj) { @@ -270,7 +282,7 @@ class Client extends OpStream { return callback(null); this.on(op => { // TODO .on .off if (this._unsynced.size === 0) { - callback(op); + callback(null, op); return OpStream.ENOUGH; } else { return OpStream.OK; diff --git a/swarm-syncable/src/Syncable.js b/swarm-syncable/src/Syncable.js index 52a7dc9..d4d3b11 100644 --- a/swarm-syncable/src/Syncable.js +++ b/swarm-syncable/src/Syncable.js @@ -33,7 +33,7 @@ class Syncable extends OpStream { this._rebuild(); if (callback) - this.once(callback); // FIXME + this.onceStateful(callback); } @@ -151,25 +151,50 @@ class Syncable extends OpStream { /** Invoke a listener after applying an op of this name * @param {String} op_name - name of the op * @param {Function} callback - listener */ - // onOp (op_name, callback) { - // this.on('.'+op_name, callback); - // } + onOp (op_name, callback) { + this.on('.'+op_name, callback); + } - /** fires once the object gets some state */ + /** Fires once the object gets some state or once that becomes unlikely. + * callback(err, obj, op). */ onceStateful (callback) { - if (!this.Version.isZero()) - callback(); - else - super.once(callback); + if (!this.Version.isZero()) return callback(); + super.once( op => { + if (this._rdt && !this.Version.isZero()) { // FIXME _rdt? + callback(null, this, op); + } else if (op.isOff()) { + callback(op.value, op, this); + } else { // FIXME no such object! + callback('object unknown', op, this); + } + }); + } + + /** Fires on every sync state event. + * Invokes callback(op), where op is either .on or .off */ + onSync (callback) { + super.on( op => { + if (!op.isOnOff()) return OpStream.OK; + callback (op); + return OpStream.ENOUGH; + }); } + /** Fires on the first sync event. */ onceSync (callback) { - super.on(function (op) { // FIXME catch reon - if (op.isOn()) { - callback (op); - return OpStream.ENOUGH; - } - return OpStream.OK; + super.on( op => { + if (!op.isOnOff()) return OpStream.OK; + callback (op); + return OpStream.ENOUGH; + }); + } + + /** Fires on the first successfull sync event. */ + onceSynced (callback) { + super.on( op => { + if (!op.isOn()) return OpStream.OK; + callback (op); + return OpStream.ENOUGH; }); } From 199e6ebb7f588285e4f3e679dc68b129b05c6bfc Mon Sep 17 00:00:00 2001 From: Victor Grishchenko Date: Tue, 8 Nov 2016 09:36:03 +0500 Subject: [PATCH 30/51] opstream arch pic --- doc/opstream-arch.png | Bin 0 -> 192240 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 doc/opstream-arch.png diff --git a/doc/opstream-arch.png b/doc/opstream-arch.png new file mode 100644 index 0000000000000000000000000000000000000000..880cc80248d9d87538c32d7da73362bd91964164 GIT binary patch literal 192240 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σ?H=tYaC239O3gA2G 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 z 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 @b8pAn@5VehmDRn|Pr18LLhW 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 o_kYvH!6W{m(f@7i|2B^6 Date: Thu, 1 Dec 2016 11:15:52 +0500 Subject: [PATCH 31/51] protocol 1.4.0 - Id --- swarm-protocol/index.js | 3 +- swarm-protocol/src/Base64x64.js | 8 +- swarm-protocol/src/Clock.js | 30 ++++--- swarm-protocol/src/{Stamp.js => Id.js} | 107 ++++++++++++++----------- swarm-protocol/src/ReplicaId.js | 15 +++- swarm-protocol/src/ReplicaIdScheme.js | 2 +- swarm-protocol/src/VV.js | 2 +- swarm-protocol/test/01_Stamp.js | 47 ++++++----- 8 files changed, 120 insertions(+), 94 deletions(-) rename swarm-protocol/src/{Stamp.js => Id.js} (67%) diff --git a/swarm-protocol/index.js b/swarm-protocol/index.js index 4a9212d..0cba631 100644 --- a/swarm-protocol/index.js +++ b/swarm-protocol/index.js @@ -2,7 +2,8 @@ var Swarm = { Base64x64: require('./src/Base64x64'), - Stamp: require('./src/Stamp'), + Id: require('./src/Id'), + Stamp: require('./src/Id'), Clock: require('./src/Clock'), Spec: require('./src/Spec'), Op: require('./src/Op'), diff --git a/swarm-protocol/src/Base64x64.js b/swarm-protocol/src/Base64x64.js index fb81f5a..ca8a4cb 100644 --- a/swarm-protocol/src/Base64x64.js +++ b/swarm-protocol/src/Base64x64.js @@ -114,7 +114,7 @@ class Base64x64 { } return [this._high, this._low]; } - + toDate () { if (this._date===null) { if (this._high===-1) { @@ -124,7 +124,7 @@ class Base64x64 { } return this._date; } - + get seq () { if (this._high===-1) { this._base2pair(); @@ -351,7 +351,8 @@ class Base64x64 { Base64x64.INFINITY = "~"; Base64x64.INCORRECT = "~~~~~~~~~~"; Base64x64.MAX32 = (1<<30)-1; -Base64x64.ZERO = "0"; // FIXME object or string?!!! +Base64x64.zero = "0"; +Base64x64.ZERO = new Base64x64(Base64x64.zero); Base64x64.rs64x64 = rs64x64; Base64x64.FULL_ZERO = '0000000000'; @@ -381,4 +382,3 @@ Base64x64.base2int = function (base) { }; module.exports = Base64x64; - diff --git a/swarm-protocol/src/Clock.js b/swarm-protocol/src/Clock.js index a034620..84a4f2a 100644 --- a/swarm-protocol/src/Clock.js +++ b/swarm-protocol/src/Clock.js @@ -1,6 +1,6 @@ "use strict"; var Base64x64 = require('./Base64x64'); -var Stamp = require('./Stamp'); +var Id = require('./Id'); /** Swarm is based on the Lamport model of time and events in a * distributed system, so Lamport logical timestamps are essential @@ -9,12 +9,14 @@ var Stamp = require('./Stamp'); * Hence, we use logical timestamps that match the wall clock time * numerically. A similar approach was termed "hybrid timestamps" * in http://www.cse.buffalo.edu/tech-reports/2014-04.pdf. + * MTproto employs some variety of plausible timestamps. * Unfortunately, we can not rely on client-side NTP, so our * approach differs in some subtle but critical ways. * The correct Swarm time is mostly dictated from upstream - * to downstream replicas, recursively. We only expect - * local clocks to have some reasonable skew, so replicas can + * to downstream replicas, recursively. As long as the + * local clock has a reasonable skew, replicas can * produce correct timestamps while being offline (temporarily). + * This borrows from the LEDBAT approach (good skew is enough). * * Again, this is logical clock that tries to be as close to * wall clock as possible, so downstream replicas may @@ -24,15 +26,17 @@ var Stamp = require('./Stamp'); * * The format of a timestamp is calendar-friendly. * Separate base64 characters denote UTC month (since Jan 2010), - * date, hour, minute, second and millisecond, plus three base64 - * chars for a sequence number: `MMDHmSssiii`. Every base64 char is + * date, hour, minute, second and millisecond, plus two + * chars for a sequence number: `MMDHmSssii`. Every base64 char is * 6 bits, but date (1-31) and hour (0-23) chars waste 1 bit each, - * so a timestamp has 64 meaningful bits in 11 base64 chars. + * while the first millisecond char wastes 2 bits. + * So, a timestamp has 56 meaningful bits in 10 base64 chars. * * We don't always need full 11 chars of a timestamp, so the class * produces Lamport timestamps of adaptable length, 5 to 11 chars, * depending on the actual event frequency. For the goals of * comparison, missing chars are interpreted as zeros. + * The most common length is 6 chars (second-precise). * The alphanumeric order of timestamps is correct, thanks to * our custom version of base64 (01...89AB...YZ_ab...yz~). * @@ -43,7 +47,7 @@ var Stamp = require('./Stamp'); class Clock { constructor (origin, meta_options) { - this._last = Stamp.ZERO; + this._last = Id.ZERO; if (!Base64x64.is(origin)) throw new Error('invalid origin'); this._origin = origin.toString(); @@ -61,7 +65,7 @@ class Clock { this._offset = parseInt(options.ClockOffst); } if (options.ClockLast) { - this._last = new Stamp(options.ClockLast); + this._last = new Id(options.ClockLast); } if (options.ClockNow) { let now = parseInt(options.ClockNow); @@ -76,13 +80,13 @@ class Clock { issueTimestamp () { var next = this._logical ? - new Stamp(this._last.Value.next(this._minlen), this._origin) : - Stamp.now(this._origin, this._offset); + new Id(this._last.Value.next(this._minlen), this._origin) : + Id.now(this._origin, this._offset); var last = this._last; if (!next.gt(last)) {// either seq++ or stuck-ahead :( next = last.next(this._origin); } else if (this._minlen<8) { // shorten? - next = new Stamp ( + next = new Id ( next.Value.relax(last.value, this._minlen), this._origin ); @@ -95,12 +99,12 @@ class Clock { return this.issueTimestamp(); } - get lastStamp () { + get lastId () { return this._last; } seeTimestamp (stamp) { - stamp = Stamp.as(stamp); + stamp = Id.as(stamp); if (stamp.gt(this._last)) { this._last = stamp; } diff --git a/swarm-protocol/src/Stamp.js b/swarm-protocol/src/Id.js similarity index 67% rename from swarm-protocol/src/Stamp.js rename to swarm-protocol/src/Id.js index af7ca70..dbb2790 100644 --- a/swarm-protocol/src/Stamp.js +++ b/swarm-protocol/src/Id.js @@ -1,11 +1,12 @@ "use strict"; -var Base64x64 = require('./Base64x64'); +const Base64x64 = require('./Base64x64'); +const ReplicaId = require('./ReplicaId'); /** * * ![events](https://upload.wikimedia.org/wikipedia/commons/thumb/5/55/Vector_Clock.svg/750px-Vector_Clock.svg.png) * - * Stamp implements Base64 string Lamport timestamps. First described in + * Id implements Base64 string Lamport timestamps. First described in * ["Time, clocks, and the ordering of events in a distributed system"][paper] * by Leslie Lamport these timestamps are designed to track events in * a distributed system. @@ -28,7 +29,7 @@ var Base64x64 = require('./Base64x64'); * * This class defines the timestamp serialization format other classes * reuse: `timestamp+replicaId`. - * Base64x64 time value followed by a separator (the plus sign) and + * Base64x64 time value followed by a separator (plus or minus sign) and * a Base64x64 process id. In our case, a "process" is a Swarm * db replica (one process runs one replica, owns one clock). * @@ -40,30 +41,31 @@ var Base64x64 = require('./Base64x64'); * [paper]: http://amturing.acm.org/p558-lamport.pdf * @class */ -class Stamp { +class Id { /** * Constructor valid parameters: - * * new Stamp("time","origin") - * * new Stamp("time+origin") - * * new Stamp("transcndnt") - * * new Stamp(stamp) - * * new Stamp() // zero + * * new Id("time","origin") + * * new Id("time-origin") + * * new Id("transcndnt") + * * new Id(stamp) + * * new Id() // zero */ constructor (stamp, origin) { this._parsed = null; + this._rid = null; this._string = null; if (origin) { this._value = Base64x64.toString(stamp); this._origin = Base64x64.toString(origin); } else if (stamp) { - if (stamp.constructor===Stamp) { + if (stamp.constructor===Id) { this._value = stamp._value; this._origin = stamp._origin; this._string = stamp._string; } else { - Stamp.reTokExt.lastIndex = 0; - var m = Stamp.reTokExt.exec(stamp.toString()); + Id.reTokExt.lastIndex = 0; + var m = Id.reTokExt.exec(stamp.toString()); if (m) { this._string = m[0]; this._value = Base64x64.toString(m[1]); @@ -92,7 +94,14 @@ class Stamp { } return this._parsed; } - + + /** ReplicaIdScheme.DEFAULT_SCHEME must be set correctly */ + get Origin () { + if (this._rid === null) + this._rid = new ReplicaId(this._origin); + return this._rid; + } + get date () { return this.Value.toDate(); } @@ -102,38 +111,34 @@ class Stamp { } static now (origin, offset) { - return new Stamp( Base64x64.now(offset), origin ); + return new Id( Base64x64.now(offset), origin ); } toString () { - return this.string; - } - - static toString (time, origin) { - return time + (origin==='0' ? '' : '+' + origin); - } - - get string () { if (this._string===null) { - this._string = Stamp.toString(this._value, this._origin); + this._string = Id.toString(this._value, this._origin); } return this._string; } + static toString (time, origin) { + return time + (origin==='0' ? '' : '-' + origin); + } + static is (str) { - Stamp.reTokExt.lastIndex = 0; - return Stamp.reTokExt.test(str); + Id.reTokExt.lastIndex = 0; + return Id.reTokExt.test(str); } // Is greater than the other stamp, according to the the lexicographic order gt (stamp) { - var s = stamp.constructor===Stamp ? stamp : new Stamp(stamp); + var s = stamp.constructor===Id ? stamp : new Id(stamp); return this._value > s._value || (this._value===s._value && this._origin>s._origin); } lt (stamp) { - var s = stamp.constructor===Stamp ? stamp : new Stamp(stamp); + var s = stamp.constructor===Id ? stamp : new Id(stamp); return this._value < s._value || (this._value===s._value && this._origin this._origin.length && s._origin.substr(0, this._origin.length) === this._origin; } isDownstreamOf (stamp) { - let s = Stamp.to(stamp); + let s = Id.to(stamp); return this._origin.length > s._origin.length && this._origin.substr(0, s._origin.length) === s._origin; } isSameOrigin (stamp) { - let s = Stamp.to(stamp); + let s = Id.to(stamp); return this._origin === s._origin; } next (origin) { let val = this.Value; - return new Stamp(val.inc().toString(), origin||this._origin); + return new Id(val.inc().toString(), origin||this._origin); } static as (val) { - if (val && val.constructor===Stamp) { + if (val && val.constructor===Id) { return val; } else { - return new Stamp(val); + return new Id(val); } } @@ -216,14 +227,14 @@ class Stamp { } -Stamp.rsTok = '=(?:\\+=)?'.replace(/=/g, Base64x64.rs64x64); -Stamp.rsTokExt = '(=)(?:[+-](=))?'.replace(/=/g, Base64x64.rs64x64); -Stamp.reTokExt = new RegExp('^'+Stamp.rsTokExt+'$'); +Id.rsTok = '=(?:\\+=)?'.replace(/=/g, Base64x64.rs64x64); +Id.rsTokExt = '(=)(?:[+-](=))?'.replace(/=/g, Base64x64.rs64x64); +Id.reTokExt = new RegExp('^'+Id.rsTokExt+'$'); -Stamp.zero = '0'; -Stamp.ZERO = new Stamp(Stamp.zero); -Stamp.never = '~'; -Stamp.NEVER = new Stamp(Stamp.never); -Stamp.ERROR = new Stamp(Base64x64.INCORRECT, '0'); +Id.zero = Base64x64.zero; +Id.ZERO = new Id(Id.zero); +Id.never = '~'; +Id.NEVER = new Id(Id.never); +Id.ERROR = new Id(Base64x64.INCORRECT, '0'); -module.exports = Stamp; +module.exports = Id; diff --git a/swarm-protocol/src/ReplicaId.js b/swarm-protocol/src/ReplicaId.js index d9c593e..a9f9ee6 100644 --- a/swarm-protocol/src/ReplicaId.js +++ b/swarm-protocol/src/ReplicaId.js @@ -10,7 +10,13 @@ class ReplicaId { * @param {ReplicaIdScheme} scheme */ constructor(id, scheme) { this._id = null; - this._scheme = new Scheme( scheme ); + if (!scheme) { + this.scheme = ReplicaId.DEFAULT_SCHEME; + } else if (scheme.constructor!==Scheme) { + this.scheme = new Scheme( scheme.toString() ); + } else { + this._scheme = scheme; + } this._parts = [null,null,null,null]; let base = null; if (id.constructor===Array) { @@ -90,4 +96,9 @@ class ReplicaId { } -module.exports = ReplicaId; \ No newline at end of file +/** Assumption: even if you open multiple databases, they still belong + to the same pool, so the replica id scheme is the same. + If you open dbs from multiple pools, you must know what you are doing. */ +ReplicaId.DEFAULT_SCHEME = new Scheme(); + +module.exports = ReplicaId; diff --git a/swarm-protocol/src/ReplicaIdScheme.js b/swarm-protocol/src/ReplicaIdScheme.js index 6ed5fe3..719e9f8 100644 --- a/swarm-protocol/src/ReplicaIdScheme.js +++ b/swarm-protocol/src/ReplicaIdScheme.js @@ -103,7 +103,7 @@ ReplicaIdScheme.CLIENT = 2; ReplicaIdScheme.SESSION = 3; ReplicaIdScheme.DB_OPTION_NAME = "DBIdScheme"; ReplicaIdScheme.FORMAT_RE = /^(\d)(\d)(\d)(\d)$/; -ReplicaIdScheme.DEFAULT_SCHEME = '0262'; +ReplicaIdScheme.DEFAULT_SCHEME = '0172'; module.exports = ReplicaIdScheme; diff --git a/swarm-protocol/src/VV.js b/swarm-protocol/src/VV.js index c0635a0..eeb0bb0 100644 --- a/swarm-protocol/src/VV.js +++ b/swarm-protocol/src/VV.js @@ -1,5 +1,5 @@ "use strict"; -var Stamp = require('./Stamp'); +var Stamp = require('./Id'); /** Version vector represented as a {origin: time} map. */ class VV { diff --git a/swarm-protocol/test/01_Stamp.js b/swarm-protocol/test/01_Stamp.js index 2a6f980..bafc998 100644 --- a/swarm-protocol/test/01_Stamp.js +++ b/swarm-protocol/test/01_Stamp.js @@ -1,45 +1,44 @@ "use strict"; -var protocol = require('..'); -var Stamp = protocol.Stamp; -var Base64x64 = protocol.Base64x64; -var tap = require('tape').test; +const protocol = require('..'); +const Id = protocol.Id; +const tap = require('tape').test; tap ('protocol.01.A Lamport timestamp', function(tap){ - var stamp1 = new Stamp('0'); + var stamp1 = new Id('0'); tap.equal(stamp1.isZero(), true, 'zero OK'); tap.equal(stamp1.toString(), '0', 'zero str OK'); - var stamp2 = new Stamp('0+gritzko'); + var stamp2 = new Id('0+gritzko'); tap.equal(stamp2.isZero(), true, 'sourced zero OK'); tap.equal(stamp2.toString(), '0+gritzko', 'zero time OK'); tap.ok(stamp2.gt(stamp1), 'fancy order'); tap.ok(stamp2.eq('0+gritzko'), 'fancy equals'); tap.ok(stamp2.eq(stamp2), 'fancy equals'); - var stamp3 = new Stamp('time', 'origin'); - tap.ok(stamp3.eq('time+origin')); - tap.ok(stamp3.toString()==='time+origin'); - tap.notOk(stamp3.eq('later+origin')); - tap.notOk(stamp3.gt('zzz+origin')); - tap.ok(stamp3.gt('time+Origin'), 'order by origin'); - tap.ok(stamp3.gt('tim+origin'), 'order by timestamp'); + var stamp3 = new Id('time', 'origin'); + tap.ok(stamp3.eq('time-origin')); + tap.ok(stamp3.toString()==='time-origin'); + tap.notOk(stamp3.eq('later-origin')); + tap.notOk(stamp3.gt('zzz-origin')); + tap.ok(stamp3.gt('time-Origin'), 'order by origin'); + tap.ok(stamp3.gt('tim-origin'), 'order by timestamp'); - var err = new Stamp('~~~~~~~~~~'); + var err = new Id('~~~~~~~~~~'); tap.ok(err.isAbnormal()); tap.ok(err.isError()); var now = new Date(); - var stamp_now = new Stamp(now, "me"); + var stamp_now = new Id(now, "me"); tap.equals(stamp_now.origin, "me"); tap.equals(stamp_now.date.getTime(), now.getTime()); - tap.ok(new Stamp('~').isNever()); - tap.ok(new Stamp('~').isTranscendent()); - tap.ok(new Stamp('0').isZero()); - tap.ok(new Stamp('0').isTranscendent()); - tap.equals(new Stamp("n0nN0","rmalizd00").toString(), "n0nN+rmalizd"); + tap.ok(new Id('~').isNever()); + tap.ok(new Id('~').isTranscendent()); + tap.ok(new Id('0').isZero()); + tap.ok(new Id('0').isTranscendent()); + tap.equals(new Id("n0nN0","rmalizd00").toString(), "n0nN-rmalizd"); tap.end(); @@ -51,10 +50,10 @@ tap ('protocol.01.B replica tree', function (t) { // assuming 1243 formula for replica ids // https://gritzko.gitbooks.io/swarm-the-protocol/content/replica.html - var primus_stamp = new Stamp("time", "P"); - var peer_stamp = new Stamp("time", "Ppr"); - var client_stamp = new Stamp("time", "Pprclnt"); - var session_stamp = new Stamp("time", "PprclntSsN"); + var primus_stamp = new Id("time", "P"); + var peer_stamp = new Id("time", "Ppr"); + var client_stamp = new Id("time", "Pprclnt"); + var session_stamp = new Id("time", "PprclntSsN"); t.ok( primus_stamp.isUpstreamOf(peer_stamp) ); t.ok( client_stamp.isDownstreamOf(peer_stamp) ); From ff5c742a1736618834a87f7054a2421b3cfa08e2 Mon Sep 17 00:00:00 2001 From: Victor Grishchenko Date: Thu, 1 Dec 2016 13:28:17 +0500 Subject: [PATCH 32/51] protocol 1.4.0 - Spec --- swarm-protocol/src/Id.js | 11 +- swarm-protocol/src/Spec.js | 234 ++++++++++++++++++++------------ swarm-protocol/test/01_Stamp.js | 2 + swarm-protocol/test/03_Spec.js | 56 ++++---- 4 files changed, 183 insertions(+), 120 deletions(-) diff --git a/swarm-protocol/src/Id.js b/swarm-protocol/src/Id.js index dbb2790..315448a 100644 --- a/swarm-protocol/src/Id.js +++ b/swarm-protocol/src/Id.js @@ -152,8 +152,9 @@ class Id { } eq (stamp) { - var s = stamp.constructor===Id ? stamp : new Id(stamp); - return this._value===s._value && this._origin===s._origin; + if (!stamp) return false; + const id = Id.as(stamp); + return this._value===id._value && this._origin===id._origin; } isTranscendent () { @@ -217,7 +218,7 @@ class Id { if (val && val.constructor===Id) { return val; } else { - return new Id(val); + return new Id(val.toString()); } } @@ -227,8 +228,8 @@ class Id { } -Id.rsTok = '=(?:\\+=)?'.replace(/=/g, Base64x64.rs64x64); -Id.rsTokExt = '(=)(?:[+-](=))?'.replace(/=/g, Base64x64.rs64x64); +Id.rsTok = '=(?:[\\+\\-]=)?'.replace(/=/g, Base64x64.rs64x64); +Id.rsTokExt = '(=)(?:[\\+\\-](=))?'.replace(/=/g, Base64x64.rs64x64); Id.reTokExt = new RegExp('^'+Id.rsTokExt+'$'); Id.zero = Base64x64.zero; diff --git a/swarm-protocol/src/Spec.js b/swarm-protocol/src/Spec.js index df15bec..bd7fec4 100644 --- a/swarm-protocol/src/Spec.js +++ b/swarm-protocol/src/Spec.js @@ -1,6 +1,6 @@ "use strict"; var Base64x64 = require('./Base64x64'); -var Stamp = require('./Stamp'); +var Id = require('./Id'); // S P E C I F I E R // @@ -19,7 +19,7 @@ var Stamp = require('./Stamp'); // the rest of the related state. For every atomic operation, be it a // field mutation or a method invocation, a specifier contains its // class, object id, a method name and, most importantly, its -// version id (see Stamp). +// version id (see Id). // // A serialized specifier is a sequence of Base64 tokens each prefixed // with a "quant". A quant for a class name is '/', an object id is @@ -42,16 +42,19 @@ class Spec { * * new Spec("/Object#1CQKn+0r1g1n!0.on") * * new Spec(["Object", "1CQKn+0r1g1n", "0", "on"]) * */ - constructor (spec, defaults) { - let t = this._toks = [Stamp.ZERO, Stamp.ZERO, Stamp.ZERO, Stamp.ZERO]; - if (!spec) { + constructor (id, type, stamp, location) { + this._id = id ? Id.as(id) : Id.ZERO; + this._type = type ? Id.as(type) : Id.ZERO; + this._stamp = stamp ? Id.as(stamp) : Id.ZERO; + this._loc = location ? Id.as(location) : Id.ZERO; + /*if (!spec) { 'nothing'; } else if (spec._toks && spec._toks.constructor===Array) { this._toks = spec._toks; } else if (spec.constructor===Array && spec.length===4) { for (let i = 0; i < 4; i++) { - var s = spec[i] || Stamp.ZERO; - t[i] = s.constructor === Stamp ? s : new Stamp(s); + var s = spec[i] || Id.ZERO; + t[i] = s.constructor === Id ? s : new Id(s); } } else { if (defaults && !defaults._toks) @@ -63,96 +66,114 @@ class Spec { } for(let i=1; i<=4; i++) { if (m[i]) { - t[i-1] = new Stamp(m[i]); + t[i-1] = new Id(m[i]); } else if (defaults) { t[i-1] = defaults._toks[i-1]; } } - } + }*/ } - get type () { - return this._toks[0].string; + static fromString (str, defaults) { + const def = defaults || Spec.ZERO; + Spec.reSpec.lastIndex = 0; + const m = Spec.reSpec.exec(str); + if (!m) throw new Error('not a specifier'); + return new Spec( + m[1]||def._id, + m[2]||def._type, + m[3]||def._stamp, + m[4]||def._loc + ); } - get id () { - return this._toks[1].string; + get Type () { + return this._type; } - get stamp () { - return this._toks[2].string; + get Id () { + return this._id; } - get name () { - return this._toks[3].string; + get Stamp () { + return this._stamp; } - get Type () { - return this._toks[0]; + get Location () { + return this._loc; } - get Id () { - return this._toks[1]; + get Loc () { + return this.Location; } - get Stamp () { - return this._toks[2]; + get type () { + return this.Type.toString(); + } + + get id () { + return this.Id.toString(); + } + + get stamp () { + return this.Stamp.toString(); } - get Name () { // FIXME sync with the spec - return this._toks[3]; + get location () { + return this.Location.toString(); + } + + get loc () { + return this.Location.toString(); } get origin () { return this.Stamp.origin; } + /** @deprecated */ get typeid () { return this.object; } - get class () { - return this.Type.value; + get typeName () { + return this.Type.isNormal() ? undefined : this.Type.value; } - get clazz () { - return this.Type.value; + get scope () { + return this.Location.isAbnormal() ? this.Location.origin : undefined; } - get scope () { - return this.Name.origin; + get eventName () { + return this.Location.isNormal() ? undefined : this.Location.value; } - get method () { - return this.Name.value; + get objectName () { + return this.Id.isNormal() ? undefined : this.Id.value; } get author () { - return this.Id.origin; + return this.Id.isNormal() ? this.Id.origin : undefined; } get birth () { - return this.Id.value; + return this.Id.isNormal() ? this.Id.value : undefined; } get time () { return this.Stamp.value; } - get type_params () { - return this.Type.origin; + get typeParameters () { + return this.Type.isAbnormal() ? this.Type.origin : undefined; } - get type_name () { // TODO rename - return this.Type.value; - } - isScoped () { - return this.scope !== Base64x64.ZERO; + return this.Location.isAbnormal() && !this.Location.isTranscendent(); } get Object () { - return new Spec([this.Type, this.Id, Stamp.ZERO, Stamp.ZERO]); + return new Spec(this.Id, this.Type, Id.ZERO, Id.ZERO); } get object () { @@ -160,53 +181,39 @@ class Spec { } get Event () { - return new Spec([Stamp.ZERO, Stamp.ZERO, this.Stamp, this.Name]); + return new Spec(Id.ZERO, Id.ZERO, this.Stamp, this.Location); } get event () { return this.Event.toString(Spec.ZERO); } + get name () { + return this.eventName; + } + toString (defaults) { + const def = defaults || Spec.ZERO; var ret = ''; - for(var i=0; i<4; i++) { - if (defaults && this._toks[i].eq(defaults._toks[i]) && (ret||i<3)) - continue; - ret += Spec.quants[i] + this._toks[i].toString(); - } + if (!this.Id.eq(def.Id)) + ret += Spec.quants[0] + this.id; + if (!this.Type.eq(def.Type)) + ret += Spec.quants[1] + this.type; + if (!this.Stamp.eq(def.Stamp)) + ret += Spec.quants[2] + this.stamp; + if (!this.Loc.eq(def.Loc) || !ret) + ret += Spec.quants[3] + this.loc; return ret; } - /** replaces 0 tokens with values from the provided Spec */ - fill (spec) { - var toks = this._toks.slice(); - var new_toks = spec.constructor===Spec ? spec._toks : spec; - for(var i=0; i<4; i++) { - if (toks[i].isZero()) { - toks[i] = new_toks[i].constructor===Stamp ? - new_toks[i] : new Stamp(new_toks[i]); - } - } - return new Spec(toks); - } - - blank (except) { - if (!except) { - except = ''; - } - var toks = this._toks.slice(); - for(var i=0; i<4; i++) { - if (except.indexOf(Spec.quants[i])===-1) { - toks[i] = Stamp.ZERO; - } - } - return new Spec(toks); - } - has (quant) { - let i = Spec.quants.indexOf(quant); - if (i===-1) { throw new Error("invalid quant"); } - return !this._toks[i].isZero(); + switch (quant) { + case Spec.quants[0]: return !this._id.isZero(); + case Spec.quants[1]: return !this._type.isZero(); + case Spec.quants[2]: return !this._stamp.isZero(); + case Spec.quants[3]: return !this._loc.isZero(); + default: throw new Error('invalid quant'); + } } static is (str) { @@ -225,31 +232,84 @@ class Spec { return this._toks.every(t => t.isEmpty()); } + + isOn () { return this.eventName === Spec.ON_OP_NAME; } + + isOff () { return this.eventName === Spec.OFF_OP_NAME; } + + isOnOff () { + return this.isOn() || this.isOff(); + } + + isHandshake () { + return this.isOnOff() && this.clazz==='Swarm'; + } + + isMutation () { // FIXME abnormal vs normal + return !this.isOnOff() && !this.isError() && !this.isState(); + } + + isState () { + return this.eventName === Spec.STATE_OP_NAME; + } + + isNoop () { + return this.eventName === Spec.NOOP_OP_NAME; + } + + isError () { + return this.eventName === Spec.ERROR_OP_NAME; + } + + isAbnormal () { + return this.Name.isAbnormal(); + } + + isNormal () { + return !this.isAbnormal(); + } + restamped (stamp, origin) { if (origin) - stamp = new Stamp(stamp, origin); - return new Spec([this.Type, this.Id, stamp, this.Name]); + stamp = new Id(stamp, origin); + return new Spec(this.Id, this.Type, stamp, this.Loc); } renamed (stamp, origin) { if (origin) - stamp = new Stamp(stamp, origin); - return new Spec([this.Type, this.Id, this.Stamp, stamp]); + stamp = new Id(stamp, origin); + return new Spec(this.Id, this.Type, this.Stamp, stamp); } /** @param {String|Base64x64} scope */ rescoped (scope) { - return new Spec([this.Type, this.Id, this.Stamp, new Stamp(this.method, scope)]); + return new Spec(this.Id, this.Type, this.Stamp, new Id(this.eventName, scope)); + } + + static as (spec) { + if (!spec) return Spec.ZERO; + if (spec.constructor===Spec) return spec; + return Spec.fromString(spec.toString()); } } -Spec.quants = ['/', '#', '!', '.']; -Spec.rsSpec = '/#!.'.replace(/./g, '(?:\\$&('+Stamp.rsTok+'))?'); +Spec.quants = "#.@:"; +Spec.rsSpec = Spec.quants.replace(/./g, '(?:\\$&('+Id.rsTok+'))?'); Spec.reSpec = new RegExp('^'+Spec.rsSpec+'$', 'g'); Spec.NON_SPECIFIC_NOOP = new Spec(); Spec.ZERO = new Spec(); -Spec.ERROR = new Spec([Stamp.ERROR, Stamp.ERROR, Stamp.ERROR, Stamp.ERROR]); +Spec.ERROR = new Spec([Id.ERROR, Id.ERROR, Id.ERROR, Id.ERROR]); + +Spec.ON_OP_NAME = "~on"; +Spec.OFF_OP_NAME = "~off"; +Spec.STATE_OP_NAME = Base64x64.INFINITY; +Spec.NOOP_OP_NAME = Base64x64.ZERO; +Spec.ERROR_OP_NAME = Base64x64.INCORRECT; +Spec.ON_STAMP = new Id(Spec.ON_OP_NAME); +Spec.OFF_STAMP = new Id(Spec.OFF_OP_NAME); +Spec.STATE_STAMP = new Id(Spec.STATE_OP_NAME); +Spec.NOOP_STAMP = new Id(Spec.NOOP_OP_NAME); +Spec.ERROR_STAMP = new Id(Spec.ERROR_OP_NAME); module.exports = Spec; - diff --git a/swarm-protocol/test/01_Stamp.js b/swarm-protocol/test/01_Stamp.js index bafc998..cb8a1bf 100644 --- a/swarm-protocol/test/01_Stamp.js +++ b/swarm-protocol/test/01_Stamp.js @@ -40,6 +40,8 @@ tap ('protocol.01.A Lamport timestamp', function(tap){ tap.ok(new Id('0').isTranscendent()); tap.equals(new Id("n0nN0","rmalizd00").toString(), "n0nN-rmalizd"); + tap.ok(Id.as("#1").eq(Id.ERROR)); + tap.end(); }); diff --git a/swarm-protocol/test/03_Spec.js b/swarm-protocol/test/03_Spec.js index 2b52c5a..5c58a16 100644 --- a/swarm-protocol/test/03_Spec.js +++ b/swarm-protocol/test/03_Spec.js @@ -7,48 +7,48 @@ const tape = require('tape').test; tape ('protocol.03.A basic specifier syntax', function (tap) { - var spec_str = '/Class#ID!7Umum+gritzkoSsn.event'; - var spec = new Spec(spec_str); + var spec_str = '#ID.json@7Umum-gritzkoSsn:event'; + var spec = Spec.fromString(spec_str); // getters - tap.equal(spec.stamp, '7Umum+gritzkoSsn'); + tap.equal(spec.stamp, '7Umum-gritzkoSsn'); tap.equal(spec.id, 'ID'); - tap.equal(spec.type, 'Class'); - tap.equal(spec.name, 'event'); + tap.equal(spec.type, 'json'); + tap.equal(spec.eventName, 'event'); tap.equal(spec.origin,'gritzkoSsn'); tap.equal(spec.Stamp.origin, 'gritzkoSsn'); tap.equal(spec.Id.value, 'ID'); - tap.equal(spec.Type.value, 'Class'); - tap.equal(spec.Name.origin, '0'); - tap.ok(spec.Name.isTranscendent()); + tap.equal(spec.Type.value, 'json'); + tap.equal(spec.Location.origin, '0'); + tap.ok(spec.Location.isTranscendent()); // toString tap.equal(spec.toString(), spec_str); // copy constructor - var spec2 = new Spec(spec); + var spec2 = new Spec(spec.Id, spec.Type, spec.stamp, spec.loc); tap.equal(spec.toString(), spec2.toString()); // fill/blank/skip - var typeid = spec.blank("/#"); - tap.equal(typeid.toString(), "/Class#ID!0.0"); - tap.equal(spec.toString(typeid), "!7Umum+gritzkoSsn.event"); - var spec3 = typeid.fill(spec.blank('!.')); - tap.equals(spec3.toString(), spec_str); + var typeid = spec.typeid; + tap.equal(typeid.toString(), "#ID.json"); + tap.equal(spec.toString(spec.Object), "@7Umum-gritzkoSsn:event"); + tap.equal(spec.event, "@7Umum-gritzkoSsn:event"); + //var spec3 = typeid.fill(spec.blank('!.')); + //tap.equals(spec3.toString(), spec_str); // immutable object reuse - tap.ok(spec.Id===spec3.Id); + //tap.ok(spec.Id===spec3.Id); // incomplete spec - var incomplete = new Spec(".on"); + var incomplete = Spec.fromString(":~on"); tap.equal(incomplete.id, '0'); tap.equal(incomplete.stamp, '0'); tap.ok(incomplete.Type.isZero()); // another spec to pase - var fieldSet = new Spec('/TodoItem+~x#7AM0f+gritzko!7AMTc+gritzko.set'); - tap.equal(fieldSet.type,'TodoItem+~x', 'type'); - tap.equal(fieldSet.id,'7AM0f+gritzko', 'id'); - tap.equal(fieldSet.stamp,'7AMTc+gritzko', 'stamp'); - tap.equal(fieldSet.name,'set'); + var fieldSet = Spec.fromString('#7AM0f-gritzko.item-~p@7AMTc-gritzko:set'); + tap.equal(fieldSet.type,'item-~p', 'type'); + tap.equal(fieldSet.id,'7AM0f-gritzko', 'id'); + tap.equal(fieldSet.stamp,'7AMTc-gritzko', 'stamp'); + tap.equal(fieldSet.eventName,'set'); - tap.equal(fieldSet.typeid, '/TodoItem+~x#7AM0f+gritzko'); - tap.equal(fieldSet.object, '/TodoItem+~x#7AM0f+gritzko'); - tap.equal(fieldSet.event, '!7AMTc+gritzko.set'); + tap.equal(fieldSet.typeid, '#7AM0f-gritzko.item-~p'); + tap.equal(fieldSet.event, '@7AMTc-gritzko:set'); tap.end(); @@ -59,10 +59,10 @@ tape ('protocol.03.B corner cases', function (tap) { var empty = new Spec(''); tap.ok(empty.type===empty.id && empty.name===empty.stamp); - tap.equal(empty.toString(Spec.ZERO), '.0'); + tap.equal(empty.toString(), ':0'); - var action = new Spec('.on+re'); - tap.equal(action.name,'on+re'); + var action = Spec.fromString(':~on-re'); + tap.equal(action.eventName,'~on'); tap.end(); @@ -71,7 +71,7 @@ tape ('protocol.03.B corner cases', function (tap) { tape ('protocol.03.f op regexes', function (t) { var reSpec = new RegExp(Spec.rsSpec); - t.ok(reSpec.test('/Swarm#db!stamp+user~ssn.on'), '.on spec'); + t.ok(reSpec.test('#db.db@stamp+user~ssn:~on'), '.on spec'); t.end(); }); From 515293950218ec00c8516fd0c7447d55b74e0eba Mon Sep 17 00:00:00 2001 From: Victor Grishchenko Date: Thu, 1 Dec 2016 22:00:37 +0500 Subject: [PATCH 33/51] Op to protocol 1.4 --- swarm-protocol/src/Op.js | 157 +++++++++++++++-------------------- swarm-protocol/src/Spec.js | 36 ++------ swarm-protocol/test/04_Op.js | 58 +++++++------ 3 files changed, 101 insertions(+), 150 deletions(-) diff --git a/swarm-protocol/src/Op.js b/swarm-protocol/src/Op.js index 9b200cb..15ea72a 100644 --- a/swarm-protocol/src/Op.js +++ b/swarm-protocol/src/Op.js @@ -1,6 +1,6 @@ "use strict"; var Base64x64 = require('./Base64x64'); -var Stamp = require('./Stamp'); +var Id = require('./Id'); var Spec = require('./Spec'); /** @@ -9,9 +9,17 @@ var Spec = require('./Spec'); * */ class Op extends Spec { - constructor (spec, value) { - super(spec); - this._value = value || ''; + /** @param {Object|String|Number|Array} value - op value (parsed) + */ + constructor (id, type, stamp, loc, value, valstr) { + super(id, type, stamp, loc); + if (value===undefined) { + this._value = valstr===undefined ? null : undefined; + this._valstr = valstr; + } else { + this._value = value; + this._valstr = valstr || undefined; + } } get spec () { @@ -23,23 +31,45 @@ class Op extends Spec { } get value () { + if (this._value===undefined) { + this._value = null; + if (this._valstr) try { + this._value = JSON.parse(this._valstr); + } catch (ex) { + console.warn('op parse error: '+ex.message); + } + } return this._value; } + get valstr () { + if (this._valstr===undefined) { + // TODO strip objects + this._valstr = this._value===null ? '' : JSON.stringify(this._value); + } + return this._valstr; + } + toString (defaults) { let ret = super.toString(defaults); - if (this._value==='') { - } else if (this._value.indexOf('\n')===-1) { - ret += '\t' + this._value; - } else { - ret += '=\n' + this._value.replace(/^(.*)$/mg, "\t$1"); - } + if (this.valstr!=='') + ret += '=' + this.valstr; return ret; } - /** whether this is not a state-mutating op */ - isPseudo () { - return Op.PSEUDO_OP_NAMES.indexOf(this.method)!==-1; + static fromString (specstr, valstr, prevop) { + const def = prevop || Op.ZERO; + Spec.reSpec.lastIndex = 0; + const m = Spec.reSpec.exec(specstr); + if (!m) throw new Error('not a specifier'); + return new Op( + m[1]||def._id, + m[2]||def._type, + m[3]||def._stamp, + m[4]||def._loc, + undefined, + valstr||'' + ); } /** parse a frame of several serialized concatenated newline- @@ -48,34 +78,20 @@ class Op extends Spec { static parseFrame (text) { var ret = []; var m = null; + var prevop = Op.ZERO; + let at = 0; Op.reOp.lastIndex = 0; - let prev; // FIXME constructor while ( m = Op.reOp.exec(text) ) { - let spec_str = m[1], - empty = m[2], - line = m[3], - lines = m[4], - length = m[5], - value; - if (!spec_str) + if (m.index!==at) + throw new Error('garbage in the input: '+text.substring(at,m.index)); + const specstr = m[1], + valstr = m[2]; + if (!specstr) continue; // empty line - if (empty!==undefined) { - value = ''; - } else if (line!==undefined) { - value = line; - } else if (lines!==undefined) { - value = lines.replace(/\n[ \t]/mg, '\n').substr(1); - } else { // explicit length - var char_length = Base64x64.classic.parse(length); - var start = Op.reOp.lastIndex; - value = text.substr(start, char_length); - if (text.charAt(start+char_length)!=='\n') { - throw new Error('unterminated op body'); - } Op.reOp.lastIndex = start+char_length; - } - let spec = new Spec(spec_str, prev); - prev = spec; - ret.push(new Op(spec, value)); + const op = Op.fromString(specstr, valstr, prevop); + ret.push(op); + prevop = op; + at = Op.reOp.lastIndex; } if (Op.reOp.lastIndex!==0) { throw new Error("mismatched content"); @@ -92,45 +108,12 @@ class Op extends Spec { frame += '\n'; // frame terminator return frame; } - - isOn () { return this.method === Op.METHOD_ON; } - - isOff () { return this.method === Op.METHOD_OFF; } - - isOnOff () { - return this.isOn() || this.isOff(); - } - - isHandshake () { - return this.isOnOff() && this.clazz==='Swarm'; - } - - isMutation () { // FIXME abnormal vs normal - return !this.isOnOff() && !this.isError() && !this.isState(); - } - - isState () { - return this.method === Op.METHOD_STATE; - } - - isNoop () { - return this.method === Op.METHOD_NOOP; - } - - isError () { - return this.method === Op.METHOD_ERROR; - } - - isNormal () { - return !this.Name.isAbnormal() && !this.isOnOff(); // TODO ~on? - } - /** * @param {String} message * @param {String|Base64x64} scope - the receiver * @returns {Op} error op */ error (message, scope) { - const Name = new Stamp(Base64x64.INCORRECT, scope || '0'); + const Name = new Id(Base64x64.INCORRECT, scope || '0'); return new Op([this.Type, this.Id, this.Stamp, Name], message); } @@ -141,8 +124,8 @@ class Op extends Spec { return new Op([ this.Type, this.Id, - new Stamp(new_stamp, this.origin), - new Stamp(this.method, this.time) + new Id(new_stamp, this.origin), + new Id(this.method, this.time) ], this._value); } @@ -152,8 +135,8 @@ class Op extends Spec { return new Op ([ this.Type, this.Id, - new Stamp(this.isScoped() ? this.scope : this.time, this.origin), - new Stamp(this.method, new_scope||'0') + new Id(this.isScoped() ? this.scope : this.time, this.origin), + new Id(this.method, new_scope||'0') ], this._value); } @@ -166,7 +149,7 @@ class Op extends Spec { this.Type, this.Id, this.Stamp, - new Stamp(this.method, scope) + new Id(this.method, scope) ], this._value); } @@ -180,7 +163,7 @@ class Op extends Spec { } static zeroStateOp (spec) { - return new Op([spec.Type, spec.Id, Stamp.ZERO, Op.METHOD_STATE], ''); + return new Op([spec.Type, spec.Id, Id.ZERO, Op.METHOD_STATE], ''); } } @@ -191,21 +174,11 @@ Op.SERIALIZATION_MODES = { EXPLICIT: 2, EXPLICIT_ONLY: 3 }; -Op.rsOp = '^\\n*(' + Spec.rsSpec.replace(/\((\?\:)?/mg, '(?:') + ')' + - '(?:(\\n)|[ \\t](.*)\\n|=$((?:\\n[ \\t].*)*)|=('+Base64x64.rs64x64+')\\n)'; +const rsSpecEsc = Spec.rsSpec.replace(/\((\?\:)?/mg, '(?:'); +Op.rsOp = '^\\n*(' + rsSpecEsc + ')' + '(?:\\=(.*))?\\n'; Op.reOp = new RegExp(Op.rsOp, "mg"); -Op.METHOD_ON = "on"; -Op.METHOD_OFF = "off"; -Op.METHOD_STATE = Base64x64.INFINITY; -Op.METHOD_NOOP = Base64x64.ZERO; -Op.METHOD_ERROR = Base64x64.INCORRECT; -Op.PSEUDO_OP_NAMES = [Op.METHOD_ON, Op.METHOD_OFF, Op.METHOD_ERROR, Op.METHOD_NOOP]; -Op.STAMP_ON = new Stamp(Op.METHOD_ON); -Op.STAMP_OFF = new Stamp(Op.METHOD_OFF); -Op.STAMP_STATE = new Stamp(Op.METHOD_STATE); -Op.STAMP_NOOP = new Stamp(Op.METHOD_NOOP); -Op.STAMP_ERROR = new Stamp(Op.METHOD_ERROR); -Op.NOTHING = new Op(new Spec(), ''); + +Op.ZERO = new Op(new Spec(), null); Op.CLASS_HANDSHAKE = "Swarm"; module.exports = Op; diff --git a/swarm-protocol/src/Spec.js b/swarm-protocol/src/Spec.js index bd7fec4..8ff0e3e 100644 --- a/swarm-protocol/src/Spec.js +++ b/swarm-protocol/src/Spec.js @@ -47,31 +47,6 @@ class Spec { this._type = type ? Id.as(type) : Id.ZERO; this._stamp = stamp ? Id.as(stamp) : Id.ZERO; this._loc = location ? Id.as(location) : Id.ZERO; - /*if (!spec) { - 'nothing'; - } else if (spec._toks && spec._toks.constructor===Array) { - this._toks = spec._toks; - } else if (spec.constructor===Array && spec.length===4) { - for (let i = 0; i < 4; i++) { - var s = spec[i] || Id.ZERO; - t[i] = s.constructor === Id ? s : new Id(s); - } - } else { - if (defaults && !defaults._toks) - throw new Error('defaults must be a Spec'); - Spec.reSpec.lastIndex = 0; - let m = Spec.reSpec.exec(spec.toString()); - if (m===null) { - throw new Error("not a specifier"); - } - for(let i=1; i<=4; i++) { - if (m[i]) { - t[i-1] = new Id(m[i]); - } else if (defaults) { - t[i-1] = defaults._toks[i-1]; - } - } - }*/ } static fromString (str, defaults) { @@ -222,10 +197,8 @@ class Spec { } isSameObject (spec) { - if (!spec._toks) { - spec = new Spec(spec); - } - return this.Type.eq(spec.Type) && this.Id.eq(spec.Id); + const s = Spec.as(spec); + return this.Type.eq(s.Type) && this.Id.eq(s.Id); } isEmpty () { @@ -289,6 +262,7 @@ class Spec { static as (spec) { if (!spec) return Spec.ZERO; if (spec.constructor===Spec) return spec; + if (spec.Id && spec.Type && spec.Stamp && spec.Loc) return spec; return Spec.fromString(spec.toString()); } @@ -303,8 +277,8 @@ Spec.ERROR = new Spec([Id.ERROR, Id.ERROR, Id.ERROR, Id.ERROR]); Spec.ON_OP_NAME = "~on"; Spec.OFF_OP_NAME = "~off"; -Spec.STATE_OP_NAME = Base64x64.INFINITY; -Spec.NOOP_OP_NAME = Base64x64.ZERO; +Spec.STATE_OP_NAME = "~state"; +Spec.NOOP_OP_NAME = Base64x64.zero; Spec.ERROR_OP_NAME = Base64x64.INCORRECT; Spec.ON_STAMP = new Id(Spec.ON_OP_NAME); Spec.OFF_STAMP = new Id(Spec.OFF_OP_NAME); diff --git a/swarm-protocol/test/04_Op.js b/swarm-protocol/test/04_Op.js index c5226aa..4a36151 100644 --- a/swarm-protocol/test/04_Op.js +++ b/swarm-protocol/test/04_Op.js @@ -1,19 +1,24 @@ "use strict"; -var swarm = require('..'); -var tape = require('tape').test; -var Op = swarm.Op; +const swarm = require('..'); +const tape = require('tape').test; +const Op = swarm.Op; +const Id = swarm.Id; tape ('protocol.04.A parse ops', function (tap) { + const empty = new Op(); + tap.equal(empty.toString(), ':0'); + + const json_op = new Op(Id.ZERO, Id.ZERO, Id.ZERO, Id.ZERO, {a:1}); + tap.equal(json_op.toString(), ':0={"a":1}'); + var parsed = Op.parseFrame ( - '/Swarm#test!timeX+author~ssn.on=\n'+ - '\tKey1: value1\n' + - ' Key2: value2\n' + - '/Model#id!stamp.set\t{"x":"y"}\n' + - '/Model#other!stamp.on\n'+ - '/Model#other!stamp.~\n' + - '/Model#other!stamp.0\n' + '#test.db@timeX-author~ssn:on={"Key1":"value1","Key2":"value2"}\n'+ + '#id.json@timeY-author:X="Y"\n' + + '#other.json@timeY-author:~on\n'+ + '#other.json@timeY-author:~state={}\n' + + '#other.json@timeY-author:0\n\n' ); tap.equal(parsed.length, 5); @@ -23,17 +28,18 @@ tape ('protocol.04.A parse ops', function (tap) { var state = parsed[3]; var noop = parsed[4]; - tap.equal(set.name, 'set'); - tap.equal(set.value, '{"x":"y"}'); + tap.equal(set.eventName, 'X'); + tap.equal(set.value, 'Y'); - tap.equal(multi.spec.origin, 'author~ssn', 'originating session'); - tap.equal(multi.spec.stamp, 'timeX+author~ssn', 'lamport tim.stamp'); - tap.equal(multi.spec.id, 'test', '#id'); - tap.equal(multi.spec.name, 'on', 'name'); - tap.equal(''+multi.spec.Stamp, 'timeX+author~ssn', 'version'); - tap.equals( multi.value.replace(/[^\n]/mg,'').length, 1 ); + tap.equal(multi.origin, 'author~ssn', 'originating session'); + tap.equal(multi.stamp, 'timeX-author~ssn', 'lamport tim.stamp'); + tap.equal(multi.id, 'test', '#id'); + tap.equal(multi.eventName, 'on', 'name'); + tap.equal(''+multi.Stamp, 'timeX-author~ssn', 'version'); + tap.equals( multi.value.Key1, "value1" ); + tap.equals( multi.value.Key2, "value2" ); - tap.ok ( state.isState() ); + tap.ok ( state.isState(), "isState" ); tap.ok ( state.isSameObject(short_on) ); tap.ok ( noop.isNoop() ); tap.notOk ( state.isNoop() ); @@ -41,14 +47,12 @@ tape ('protocol.04.A parse ops', function (tap) { tap.notOk ( noop.isSameObject(set) ); tap.equal(multi.toString(), - '/Swarm#test!timeX+author~ssn.on=\n'+ - '\tKey1: value1\n' + - '\tKey2: value2', - 'multiline serialization'); - - tap.equal(short_on.toString(), '/Model#other!stamp.on'); - tap.equal(short_on.value, ''); - tap.equal(short_on.name, 'on'); + '#test.db@timeX-author~ssn:on={"Key1":"value1","Key2":"value2"}', + 'serialization'); + + tap.equal(short_on.toString(), '#other.json@timeY-author:~on'); + tap.equal(short_on.value, null); + tap.equal(short_on.name, '~on'); tap.ok( short_on.spec.Type.isTranscendent() ); tap.end(); From 5130a0d01f1e2765aa4de464d7b08e40989ece6e Mon Sep 17 00:00:00 2001 From: Victor Grishchenko Date: Thu, 1 Dec 2016 22:01:11 +0500 Subject: [PATCH 34/51] Id.next() test --- swarm-protocol/test/01_Stamp.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/swarm-protocol/test/01_Stamp.js b/swarm-protocol/test/01_Stamp.js index cb8a1bf..9527e3b 100644 --- a/swarm-protocol/test/01_Stamp.js +++ b/swarm-protocol/test/01_Stamp.js @@ -42,6 +42,10 @@ tap ('protocol.01.A Lamport timestamp', function(tap){ tap.ok(Id.as("#1").eq(Id.ERROR)); + const one = Id.as('0000000010-one'); + const two = one.next('two'); + tap.ok(two.eq('0000000011-two')); + tap.end(); }); From 3e879807709bc25495247390db996f66d8d57b99 Mon Sep 17 00:00:00 2001 From: Victor Grishchenko Date: Thu, 1 Dec 2016 22:56:48 +0500 Subject: [PATCH 35/51] Op.js 94% test coverage --- swarm-protocol/src/Op.js | 53 +++++++++++------------------------- swarm-protocol/test/04_Op.js | 38 +++++++++++++++++++++++--- 2 files changed, 50 insertions(+), 41 deletions(-) diff --git a/swarm-protocol/src/Op.js b/swarm-protocol/src/Op.js index 15ea72a..058ea45 100644 --- a/swarm-protocol/src/Op.js +++ b/swarm-protocol/src/Op.js @@ -7,7 +7,7 @@ var Spec = require('./Spec'); * Immutable Swarm op, see the specification at * https://gritzko.gitbooks.io/swarm-the-protocol/content/op.html * */ -class Op extends Spec { +class Op extends Spec { // FIXME EAT SPEC /** @param {Object|String|Number|Array} value - op value (parsed) */ @@ -99,11 +99,11 @@ class Op extends Spec { return ret; } - static serializeFrame (ops, prev_spec) { + static serializeFrame (ops, prev_op) { let frame = ''; ops.forEach( op => { - frame += op.toString(prev_spec) + '\n'; - prev_spec = op.spec; + frame += op.toString(prev_op) + '\n'; + prev_op = op; }); frame += '\n'; // frame terminator return frame; @@ -114,56 +114,35 @@ class Op extends Spec { * @returns {Op} error op */ error (message, scope) { const Name = new Id(Base64x64.INCORRECT, scope || '0'); - return new Op([this.Type, this.Id, this.Stamp, Name], message); - } - - /** @param {Base64x64|String} new_stamp */ - overstamped (new_stamp) { - if (this.isScoped()) - throw new Error('can not overstamp a scoped op'); - return new Op([ - this.Type, - this.Id, - new Id(new_stamp, this.origin), - new Id(this.method, this.time) - ], this._value); - } - - clearstamped (new_scope) { - if (!this.isScoped() && !new_scope) - return this; - return new Op ([ - this.Type, - this.Id, - new Id(this.isScoped() ? this.scope : this.time, this.origin), - new Id(this.method, new_scope||'0') - ], this._value); + return new Op(this.Id, this.Type, this.Stamp, Name, message); } stamped (stamp) { - return new Op([this.Type, this.Id, stamp, this.Name], this._value); + return new Op(this.Type, this.Id, stamp, this.Name, this._value); } scoped (scope) { - return new Op([ - this.Type, + return new Op( this.Id, + this.Type, this.Stamp, new Id(this.method, scope) - ], this._value); + , this._value); } named (name, value) { - return new Op([ - this.Type, + return new Op( this.Id, + this.Type, this.Stamp, - name - ], value || this._value); + name, + value || this._value + ); } static zeroStateOp (spec) { - return new Op([spec.Type, spec.Id, Id.ZERO, Op.METHOD_STATE], ''); + const s = Spec.as (spec); + return new Op(s.Id, s.Type, Id.ZERO, Spec.STATE_OP_NAME, null); } } diff --git a/swarm-protocol/test/04_Op.js b/swarm-protocol/test/04_Op.js index 4a36151..6278fa7 100644 --- a/swarm-protocol/test/04_Op.js +++ b/swarm-protocol/test/04_Op.js @@ -3,6 +3,7 @@ const swarm = require('..'); const tape = require('tape').test; const Op = swarm.Op; const Id = swarm.Id; +const Spec = swarm.Spec; tape ('protocol.04.A parse ops', function (tap) { @@ -16,12 +17,22 @@ tape ('protocol.04.A parse ops', function (tap) { var parsed = Op.parseFrame ( '#test.db@timeX-author~ssn:on={"Key1":"value1","Key2":"value2"}\n'+ '#id.json@timeY-author:X="Y"\n' + - '#other.json@timeY-author:~on\n'+ - '#other.json@timeY-author:~state={}\n' + - '#other.json@timeY-author:0\n\n' + '#other.json@timeZ-author:~on\n'+ + '#other.json@timeZ-author:~state={}\n' + + '#other.json@timeZ-author:0="invalid\n\n' ); tap.equal(parsed.length, 5); + + const serial = Op.serializeFrame(parsed); + tap.equal(serial, + '#test.db@timeX-author~ssn:on={"Key1":"value1","Key2":"value2"}\n'+ + '#id.json@timeY-author:X="Y"\n' + + '#other@timeZ-author:~on\n'+ + ':~state={}\n' + + ':0="invalid\n\n' + ); + var multi = parsed[0]; var set = parsed[1]; var short_on = parsed[2]; @@ -50,12 +61,31 @@ tape ('protocol.04.A parse ops', function (tap) { '#test.db@timeX-author~ssn:on={"Key1":"value1","Key2":"value2"}', 'serialization'); - tap.equal(short_on.toString(), '#other.json@timeY-author:~on'); + tap.equal(short_on.toString(), '#other.json@timeZ-author:~on'); tap.equal(short_on.value, null); tap.equal(short_on.name, '~on'); tap.ok( short_on.spec.Type.isTranscendent() ); + tap.equal(noop.value, null); // invalid value, ignored + tap.end(); }); +tape ('protocol.04.B ops - mutators', function (tap) { + + const empty = new Op(); + const scoped = empty.scoped('R'); + tap.equal(scoped.toString(), ':0-R') + const stamped = empty.stamped('time-origin'); + tap.equal(stamped.toString(), '@time-origin') + const named = stamped.named(Spec.ERROR_OP_NAME); + tap.equal(named.toString(), '@time-origin:~~~~~~~~~~') + const error = stamped.error('message', 'R'); + tap.equal(error.toString(), "@time-origin:~~~~~~~~~~-R=\"message\""); + const zero = Op.zeroStateOp("#id.type"); + tap.equal(zero.toString(), "#id.type:~state"); + + tap.end(); + +}); \ No newline at end of file From 5080540f7938d3203676301afaf963715330174d Mon Sep 17 00:00:00 2001 From: Victor Grishchenko Date: Fri, 2 Dec 2016 00:10:38 +0500 Subject: [PATCH 36/51] protocol: green test coverage --- swarm-protocol/src/ReplicaId.js | 5 +-- swarm-protocol/src/Spec.js | 29 ++++------------- swarm-protocol/src/VV.js | 20 ++++-------- swarm-protocol/test/00_base64.js | 25 +++++++++++++-- swarm-protocol/test/01_Stamp.js | 42 ++++++++++++++++++++++++ swarm-protocol/test/02_Clock.js | 3 +- swarm-protocol/test/03_Spec.js | 32 ++++++++++++++++++- swarm-protocol/test/05_VV.js | 48 ++++++++++++++++------------ swarm-protocol/test/06_replica_id.js | 3 ++ 9 files changed, 145 insertions(+), 62 deletions(-) diff --git a/swarm-protocol/src/ReplicaId.js b/swarm-protocol/src/ReplicaId.js index a9f9ee6..81931ac 100644 --- a/swarm-protocol/src/ReplicaId.js +++ b/swarm-protocol/src/ReplicaId.js @@ -5,15 +5,16 @@ const Scheme = require('./ReplicaIdScheme'); /** Replica id, immutable. * https://gritzko.gitbooks.io/swarm-the-protocol/content/replica.html */ class ReplicaId { + // FIXME as, is, all the immutable conventions PLEASE!!! /** @param {Base64x64|String|Array} id * @param {ReplicaIdScheme} scheme */ constructor(id, scheme) { this._id = null; if (!scheme) { - this.scheme = ReplicaId.DEFAULT_SCHEME; + this._scheme = ReplicaId.DEFAULT_SCHEME; } else if (scheme.constructor!==Scheme) { - this.scheme = new Scheme( scheme.toString() ); + this._scheme = new Scheme( scheme.toString() ); } else { this._scheme = scheme; } diff --git a/swarm-protocol/src/Spec.js b/swarm-protocol/src/Spec.js index 8ff0e3e..e72c8cf 100644 --- a/swarm-protocol/src/Spec.js +++ b/swarm-protocol/src/Spec.js @@ -202,10 +202,10 @@ class Spec { } isEmpty () { - return this._toks.every(t => t.isEmpty()); + return this.Id.isZero() && this.Type.isZero() && + this.Stamp.isZero() && this.Location.isZero(); } - isOn () { return this.eventName === Spec.ON_OP_NAME; } isOff () { return this.eventName === Spec.OFF_OP_NAME; } @@ -215,11 +215,11 @@ class Spec { } isHandshake () { - return this.isOnOff() && this.clazz==='Swarm'; + return this.isOnOff() && this.Type.value==='db'; // TODO constant } - isMutation () { // FIXME abnormal vs normal - return !this.isOnOff() && !this.isError() && !this.isState(); + isMutation () { + return ! Base64x64.isAbnormal(this.Location.value); } isState () { @@ -235,30 +235,13 @@ class Spec { } isAbnormal () { - return this.Name.isAbnormal(); + return this.Location.isAbnormal(); } isNormal () { return !this.isAbnormal(); } - restamped (stamp, origin) { - if (origin) - stamp = new Id(stamp, origin); - return new Spec(this.Id, this.Type, stamp, this.Loc); - } - - renamed (stamp, origin) { - if (origin) - stamp = new Id(stamp, origin); - return new Spec(this.Id, this.Type, this.Stamp, stamp); - } - - /** @param {String|Base64x64} scope */ - rescoped (scope) { - return new Spec(this.Id, this.Type, this.Stamp, new Id(this.eventName, scope)); - } - static as (spec) { if (!spec) return Spec.ZERO; if (spec.constructor===Spec) return spec; diff --git a/swarm-protocol/src/VV.js b/swarm-protocol/src/VV.js index eeb0bb0..d84eb94 100644 --- a/swarm-protocol/src/VV.js +++ b/swarm-protocol/src/VV.js @@ -17,8 +17,8 @@ class VV { var stamps = []; this.map.forEach((t, o) => stamps.push(Stamp.toString(t, o))); stamps.sort().reverse(); - stamps.unshift(stamps.length?'':'!0'); - return stamps.join('!'); + stamps.unshift(stamps.length?'':'@0'); + return stamps.join('@'); } // @@ -40,12 +40,6 @@ class VV { this._max = value; } - remove (origin) { // FIXME remove this op - console.warn('VV.remove() is deprecated'); - origin = VV.norm_src(origin); - this.map.delete(origin); - return this; - } delete (origin) { this.remove(origin); @@ -68,12 +62,12 @@ class VV { get (origin) { origin = VV.norm_src(origin); var time = this.map.get(origin); - return time ? time + '+' + origin : '0'; + return time ? time + '-' + origin : '0'; } has (origin) { origin = VV.norm_src(origin); - return this.map.hasOwnProperty(origin); + return this.map.has(origin); } covers (version) { @@ -89,7 +83,7 @@ class VV { vv = new VV(vv); } for(var origin of vv.map.keys()) { - if ( this.get(origin) > this.get(origin) ) + if ( vv.get(origin) > this.get(origin) ) return false; } return true; @@ -100,7 +94,7 @@ class VV { } static norm_src (origin) { - if (origin.constructor===String && origin.indexOf('+')!==-1) { + if (origin.constructor===String && origin.indexOf('-')!==-1) { return new Stamp(origin).origin; } else { return origin; @@ -109,7 +103,7 @@ class VV { } -VV.rsVVTok = '!'+Stamp.rsTokExt; +VV.rsVVTok = '@'+Stamp.rsTokExt; VV.reVVTok = new RegExp(VV.rsVVTok, 'g'); module.exports = VV; diff --git a/swarm-protocol/test/00_base64.js b/swarm-protocol/test/00_base64.js index 0aabbb2..877c4ca 100644 --- a/swarm-protocol/test/00_base64.js +++ b/swarm-protocol/test/00_base64.js @@ -75,7 +75,7 @@ tap('protocol.00.A basic API', function (t) { tap ('protocol.00.B base64 conversions perf', function(tap){ var ms1 = new Date().getTime(); - var count = 1000000; + var count = 100000; for(var i=0; its1.value) tap.equal(ts2.value.substr(0,6), '000002'); tap.equal(ts2.origin, 'leslie'); diff --git a/swarm-protocol/test/03_Spec.js b/swarm-protocol/test/03_Spec.js index 5c58a16..48f3c8c 100644 --- a/swarm-protocol/test/03_Spec.js +++ b/swarm-protocol/test/03_Spec.js @@ -69,9 +69,39 @@ tape ('protocol.03.B corner cases', function (tap) { }); -tape ('protocol.03.f op regexes', function (t) { +tape ('protocol.03.C op regexes', function (t) { var reSpec = new RegExp(Spec.rsSpec); t.ok(reSpec.test('#db.db@stamp+user~ssn:~on'), '.on spec'); t.end(); }); +tape ('protocol.03.D testers', function (t) { + + t.ok(Spec.as(':~~~~~~~~~~').isError()); + t.ok(Spec.as(':~state').isState()); + t.ok(Spec.as(':~on').isOn()); + t.ok(!Spec.as(':~on').isOff()); + t.ok(Spec.as(':~on').isOnOff()); + t.ok(Spec.as(':~off').isOnOff()); + t.ok(Spec.as(':~off').isOff()); + t.ok(Spec.as(':~off').isAbnormal()); + t.notOk(Spec.as(':~off').isNormal()); + t.ok(Spec.ZERO.isEmpty()); + t.notOk(Spec.as(':1').isEmpty()); + t.notOk(Spec.as(':1').isHandshake()); + t.ok(Spec.as('#test.db:~on').isHandshake()); + t.notOk(Spec.as('#test.db:~on').isMutation()); + t.ok(Spec.as('#test.db:IdScheme').isMutation()); + + t.ok(Spec.as('#test.db:IdScheme').has('#')); + t.ok(Spec.as('#test.db:IdScheme').has('.')); + t.ok(Spec.as('#test.db:IdScheme').has(':')); + t.notOk(Spec.as('#test.db:IdScheme').has('@')); + + t.ok(Spec.is('#test.db:IdScheme')); + t.notOk(Spec.is('#test.db:IdScheme?')); + + t.end(); +}); + + diff --git a/swarm-protocol/test/05_VV.js b/swarm-protocol/test/05_VV.js index d3332bb..5a1191f 100644 --- a/swarm-protocol/test/05_VV.js +++ b/swarm-protocol/test/05_VV.js @@ -5,26 +5,34 @@ var tape = require('tape').test; tape ('protocol.05.A VV basics', function (tap) { - var vec = '!7AM0f+gritzko!0longago+krdkv!7AMTc+aleksisha!some+garbage'; + var vec = '@7AM0f-gritzko@0longago-krdkv@7AMTc-aleksisha@some-garbage'; var vv = new VV(vec); - tap.ok(vv.covers('7AM0f+gritzko')); - tap.ok(!vv.covers('7AMTd+aleksisha')); - tap.ok(!vv.covers('6AMTd+maxmaxmax')); + tap.ok(vv.covers('7AM0f-gritzko')); + tap.ok(!vv.covers('7AMTd-aleksisha')); + tap.ok(!vv.covers('6AMTd-maxmaxmax')); tap.equal(vv.toString(), - '!some+garbage!7AMTc+aleksisha!7AM0f+gritzko!0longago+krdkv'); + '@some-garbage@7AMTc-aleksisha@7AM0f-gritzko@0longago-krdkv'); + tap.ok(vv.coversAll(vv)); - var map2 = new VV("!1QDpv03+anon000qO!1P7AE05+anon000Bu"); - tap.equal(map2.covers('1P7AE05+anon000Bu'), true, 'covers the border'); + var map2 = new VV("@1QDpv03-anon000qO@1P7AE05-anon000Bu"); + tap.equal(map2.covers('1P7AE05-anon000Bu'), true, 'covers the border'); - var one = new VV('!1+one!2+two!0+three'); - var two = new VV('!0+two'); - var three = '!3+three'; + var one = new VV('@1-one@2-two@0-three'); + var two = new VV('@0-two'); + var three = '@3-three'; var add = one.addAll(two).addAll(three); - tap.equal(add.toString(), '!3+three!2+two!1+one'); + tap.equal(add.toString(), '@3-three@2-two@1-one'); tap.equals(one.max, '3'); tap.equals(vv.max, 'some'); + const redundant = new VV('@1-me@2-me'); + tap.equal(redundant.toString(), '@2-me'); + tap.equal(redundant.get('me'), '2-me'); + tap.ok(redundant.has('me')); + tap.notOk(redundant.has('notme')); + tap.notOk(vv.coversAll(redundant)); + tap.end(); }); @@ -32,17 +40,17 @@ tape ('protocol.05.A VV basics', function (tap) { tape ('protocol.05.C zero vector', function (tap) { var empty = new VV(); - tap.equal(empty.toString(), '!0', 'empty vector is !0'); - tap.ok(empty.covers('0'), '!0 covers 0'); - tap.ok(!empty.covers('1+a'), '!0 covers nothing'); + tap.equal(empty.toString(), '@0', 'empty vector is @0'); + tap.ok(empty.covers('0'), '@0 covers 0'); + tap.ok(!empty.covers('1-a'), '@0 covers nothing'); - var empty2 = new VV('!0'); - tap.equal(empty2.toString(), '!0'); + var empty2 = new VV('@0'); + tap.equal(empty2.toString(), '@0'); - var empty3 = new VV('!a+b!c+d'); - empty3 = empty3.remove('b').remove('c+d'); - tap.equal(empty3.toString(), '!0'); - tap.equals(empty3.max, '0'); + var empty3 = new VV('@a-b@c-d'); + tap.equals(empty3.max, 'c'); + empty3.add('e-f'); + tap.equals(empty3.max, 'e'); tap.end(); }); diff --git a/swarm-protocol/test/06_replica_id.js b/swarm-protocol/test/06_replica_id.js index 3331611..a4c0076 100644 --- a/swarm-protocol/test/06_replica_id.js +++ b/swarm-protocol/test/06_replica_id.js @@ -36,6 +36,9 @@ tape ('protocol.06.B replica id', function (tap) { tap.equals(id1.peer, '0AB'); tap.equals(id1.client, '000user'); tap.equals(id1.session, '0000000003'); + tap.notOk(id1.isPeer()); + tap.ok(id1.isClient()); + tap.ok(id1.isClientOf(new ReplicaId('1AB', scheme))); tap.ok(Base64x64.is(id1)); const id2 = new ReplicaId('1ABuser', scheme); From 88b8116e42f43a8146c90317cb14d833103c5822 Mon Sep 17 00:00:00 2001 From: Victor Grishchenko Date: Fri, 2 Dec 2016 12:54:40 +0500 Subject: [PATCH 37/51] Immutable ReplicaId; coverage: all green --- swarm-protocol/src/Base64x64.js | 10 +++ swarm-protocol/src/Id.js | 2 +- swarm-protocol/src/ReplicaId.js | 108 ++++++++++++-------------- swarm-protocol/src/ReplicaIdScheme.js | 8 +- swarm-protocol/test/06_replica_id.js | 18 +++-- 5 files changed, 76 insertions(+), 70 deletions(-) diff --git a/swarm-protocol/src/Base64x64.js b/swarm-protocol/src/Base64x64.js index ca8a4cb..8160277 100644 --- a/swarm-protocol/src/Base64x64.js +++ b/swarm-protocol/src/Base64x64.js @@ -346,6 +346,16 @@ class Base64x64 { return new Base64x64(Base64x64.FULL_ZERO.substr(0, offset)+this._base.substr(offset, length)); } + static isZero (b) { + return Base64x64.as(b).isZero(); + } + + static as (b) { + if (!b) return Base64x64.ZERO; + if (b.constructor===Base64x64) return b; + return new Base64x64(b.toString()); + } + } Base64x64.INFINITY = "~"; diff --git a/swarm-protocol/src/Id.js b/swarm-protocol/src/Id.js index 315448a..62c0c31 100644 --- a/swarm-protocol/src/Id.js +++ b/swarm-protocol/src/Id.js @@ -98,7 +98,7 @@ class Id { /** ReplicaIdScheme.DEFAULT_SCHEME must be set correctly */ get Origin () { if (this._rid === null) - this._rid = new ReplicaId(this._origin); + this._rid = ReplicaId.fromString(this._origin); return this._rid; } diff --git a/swarm-protocol/src/ReplicaId.js b/swarm-protocol/src/ReplicaId.js index 81931ac..25989e8 100644 --- a/swarm-protocol/src/ReplicaId.js +++ b/swarm-protocol/src/ReplicaId.js @@ -5,74 +5,49 @@ const Scheme = require('./ReplicaIdScheme'); /** Replica id, immutable. * https://gritzko.gitbooks.io/swarm-the-protocol/content/replica.html */ class ReplicaId { + // FIXME as, is, all the immutable conventions PLEASE!!! + // 2. fromString + // 3. is + // 4. as (str) + // 5. forkPeer, forkClient, forkSession - /** @param {Base64x64|String|Array} id - * @param {ReplicaIdScheme} scheme */ - constructor(id, scheme) { - this._id = null; - if (!scheme) { - this._scheme = ReplicaId.DEFAULT_SCHEME; - } else if (scheme.constructor!==Scheme) { - this._scheme = new Scheme( scheme.toString() ); - } else { - this._scheme = scheme; - } - this._parts = [null,null,null,null]; - let base = null; - if (id.constructor===Array) { - if (id.length!==4) - throw new Error("need all 4 parts"); - this._parts = id.map( (val, p) => this._scheme.slice(val, p) ); - this._rebuild(); - } else { - base = new Base64x64(id); - this._id = base.toString(); - this._parts = this._scheme.split(this._id); - } + /** */ + constructor(primus, peer, client, ssn, scheme) { + this._scheme = scheme ? Scheme.as(scheme) : ReplicaId.SCHEME; + this._id = this._scheme.join([primus, peer, client, ssn]); + const parts = this._scheme.split(this._id); + this._primus = parts[0]; + this._peer = parts[1]; + this._client = parts[2]; + this._ssn = parts[3]; } - _rebuild () { - this._id = this._scheme.join(this._parts); - return this; + static fromString (id, scheme) { + const sch = scheme ? Scheme.as(scheme) : ReplicaId.SCHEME; + if (!ReplicaId.is(id)) + throw new Error('not a replica id'); + const parts = sch.split(id); + return new ReplicaId(parts[0], parts[1], parts[2], parts[3], sch); } - /** @param {Array} parts - * @param {ReplicaIdScheme} scheme */ - static createId (parts, scheme) { - let full = ''; - for(let p=0, off=0; p<4; p++) { - const len = scheme.partLength(p); - if (len===0) continue; - let segment = parts[p].substr(off, len) || '0'; - while (segment.length0; @@ -103,7 +109,7 @@ ReplicaIdScheme.CLIENT = 2; ReplicaIdScheme.SESSION = 3; ReplicaIdScheme.DB_OPTION_NAME = "DBIdScheme"; ReplicaIdScheme.FORMAT_RE = /^(\d)(\d)(\d)(\d)$/; -ReplicaIdScheme.DEFAULT_SCHEME = '0172'; +ReplicaIdScheme.DEFAULT_SCHEME = new ReplicaIdScheme('0172'); module.exports = ReplicaIdScheme; diff --git a/swarm-protocol/test/06_replica_id.js b/swarm-protocol/test/06_replica_id.js index a4c0076..3141473 100644 --- a/swarm-protocol/test/06_replica_id.js +++ b/swarm-protocol/test/06_replica_id.js @@ -31,31 +31,33 @@ tape ('protocol.06.B replica id', function (tap) { const scheme = new ReplicaIdScheme(1261); - const id1 = new ReplicaId('1ABuser003', scheme); + const id1 = ReplicaId.fromString('1ABuser003', scheme); tap.equals(id1.primus, '1'); tap.equals(id1.peer, '0AB'); tap.equals(id1.client, '000user'); tap.equals(id1.session, '0000000003'); tap.notOk(id1.isPeer()); tap.ok(id1.isClient()); - tap.ok(id1.isClientOf(new ReplicaId('1AB', scheme))); + tap.ok(id1.isClientOf(ReplicaId.fromString('1AB', scheme))); tap.ok(Base64x64.is(id1)); - const id2 = new ReplicaId('1ABuser', scheme); + const id2 = ReplicaId.fromString('1ABuser', scheme); tap.equals(id2.primus, '1'); tap.equals(id2.peer, '0AB'); tap.equals(id2.client, '000user'); tap.equals(id2.session, '0'); tap.ok(Base64x64.is(id2)); - const id3 = ReplicaId.createId(['P', '0ee', '000client', '000000000S'], scheme); + const id3 = new ReplicaId('P', '0ee', '000client', '000000000S', scheme); tap.equals(id3.toString(), 'PeeclientS'); tap.ok(Base64x64.is(id3)); - const id4 = new ReplicaId("0client", "163"); - id4.session = '0000000SSN'; - id4.peer = 'R'; - tap.equals( id4+'', 'RclientSSN' ); + const id4 = ReplicaId.fromString("R", "163"); + tap.equals( id4+'', 'R' ); + const id5= id4.forkClient('0client'); + tap.equals( id5+'', 'Rclient' ); + const id6 = id5.forkSession('0000000123'); + tap.equals( id6+'', 'Rclient123' ); tap.end(); }); From f4b5db45aa7a7eed288a560858be03c5e694dcc9 Mon Sep 17 00:00:00 2001 From: Victor Grishchenko Date: Mon, 5 Dec 2016 20:46:59 +0500 Subject: [PATCH 38/51] first shot at Ids --- swarm-protocol/index.js | 1 + swarm-protocol/src/Ids.js | 242 ++++++++++++++++++++++++++++++++++ swarm-protocol/test/07_ids.js | 35 +++++ 3 files changed, 278 insertions(+) create mode 100644 swarm-protocol/src/Ids.js create mode 100644 swarm-protocol/test/07_ids.js diff --git a/swarm-protocol/index.js b/swarm-protocol/index.js index 0cba631..6bc28e6 100644 --- a/swarm-protocol/index.js +++ b/swarm-protocol/index.js @@ -8,6 +8,7 @@ var Swarm = { Spec: require('./src/Spec'), Op: require('./src/Op'), VV: require('./src/VV'), + Ids: require('./src/Ids'), ReplicaId: require('./src/ReplicaId'), ReplicaIdScheme: require('./src/ReplicaIdScheme') }; diff --git a/swarm-protocol/src/Ids.js b/swarm-protocol/src/Ids.js new file mode 100644 index 0000000..74f87d7 --- /dev/null +++ b/swarm-protocol/src/Ids.js @@ -0,0 +1,242 @@ +"use strict"; +const Id = require('./Id'); +const Base64x64 = require('./Base64x64'); + +/** immutable id array */ +class Ids { + + constructor (body) { + this._body = body || ''; + } + + static fromString (str) { + return new Ids(str); + } + + toString() { + return this._body; + } + + static as (ids) { + if (!ids) return new ids(); + if (ids.constructor) return ids; + return new Ids(ids); + } + + static is (ids) { + return Ids.ids_re.test(ids); + } + + // --- access/edit API --- + + /** @returns {Ids} -- new array */ + splice (offset, del_count, inserts) { + const b = new Builder(); + + const i = this.iterator(); + // append runs + while (!i.end() && i.runEndOffset()<=offset) { + body += i.runString(); + i.nextRun(); + } + // open split run + // add that many + while (!i.end() && i.offset<=offset) { + append(i.id()); + i.next(); + } + // add new + inserts.forEach(append); + // skip the deleted + for(let i=0; ithis.prefixlen+2) { + this._flushRun(); + return this.append(id); + } + let two = val.substr(this.prefixlen, this.prefixlen+2); + while (two.length<2) two += '0'; + this.tail += two; + this.runlen++; // vvv + } + + _appendToEmptyRun (id) { + // try start a run + const iv = id.value; + const liv = this.last_id.value; + if (iv===liv) { + this.runtype = Ids.UNI_RUN; + this.runlen = 1; + return this._appendToUniRun(id); + } + const prefix = Base64x64.commonPrefix(iv, liv); + if (iv.length<=prefix.length+2 && liv.length<=prefix.length+2) { + this.runtype = Ids.LAST2_RUN; + this.prefixlen = prefix.length; + this.runlen = 1; + this.prefix = prefix; + return this._appendToLast2Run(id); + } + // if nothing worked + this._flushRun(); + this.last_id = id; + } + + append (id) { + id = Id.as(id); + if (!this.last_id) { + this.last_id = id; + this.runlen = 1; + } else if (id.origin!==this.last_id.origin) { + this._flushRun(); + this.last_id = id; + this.runlen = 1; + } else if (this.runtype===Ids.LAST2_RUN) { + this._appendToLast2Run(id); + } else if (this.runtype===Ids.UNI_RUN) { + this._appendToUniRun(id); + } else { + this._appendToEmptyRun(id); + } + } + + toString () { + this._flushRun(); + return this.body.join(''); + } +} + +Ids.UNI_RUN = ';'; +Ids.LAST_RUN = ','; +Ids.LAST2_RUN = "'"; +Ids.INC_RUN = '"'; + + +class Iterator { + constructor (id) { + this.ids; + this.ids_offset; + this.head; + this.method; + this.body; + this.run_offset; + this.id; + } + id () { + + } + next () { + + } + nextRun () { + + } + runHas (id) { + + } + + append (id) { + + } + /** @returns {Id} -- id at the pos */ + at (offset) { + + } + toString () { + + } + fromString (str, offset) { + const pos = offset || 0; + + } + fromMatch (head, method, body) { + + } +} + +Ids.Builder = Builder; +Ids.Iterator = Iterator; +module.exports = Ids; \ No newline at end of file diff --git a/swarm-protocol/test/07_ids.js b/swarm-protocol/test/07_ids.js new file mode 100644 index 0000000..71d98c6 --- /dev/null +++ b/swarm-protocol/test/07_ids.js @@ -0,0 +1,35 @@ +"use strict"; +const protocol = require('..'); +const Id = protocol.Id; +const Ids = protocol.Ids; +const tap = require('tape').test; + + +tap ('protocol.07.A builder', function(tap) { + + const b = new Ids.Builder(); + b.append(Id.ZERO); + b.append(Id.ZERO); + b.append(Id.ZERO); + tap.equal(b.toString(), "@0;3"); + + const b2 = new Ids.Builder(); + b2.append("ABCDEF-author"); + b2.append("ABCDGH-author"); + b2.append("ABCDIJ-author"); + b2.append("ABCDKLM-author"); + b2.append("ABCDKNO-author"); + b2.append("ABCDKN-author"); + b2.append("ABCDKNP-author"); + b2.append("ABCDKQR-author"); + b2.append("ABCDKQR-other"); + tap.equal(b2.toString(), + "@ABCDEF-author'GHIJ@ABCDKLM-author'NON0NPQR@ABCDKQR-other" + ); + + tap.end(); +}); + +// tap ('protocol.07.B iterator', function(tap) { +// +// }); \ No newline at end of file From 3453c7c60d1e9ce2c66e77c2723cdd9271a522c1 Mon Sep 17 00:00:00 2001 From: Victor Grishchenko Date: Tue, 6 Dec 2016 11:03:21 +0500 Subject: [PATCH 39/51] Ids.Iterator 1st approach --- swarm-protocol/src/Ids.js | 112 ++++++++++++++++++++++++---------- swarm-protocol/test/07_ids.js | 68 ++++++++++++++++----- 2 files changed, 134 insertions(+), 46 deletions(-) diff --git a/swarm-protocol/src/Ids.js b/swarm-protocol/src/Ids.js index 74f87d7..9c51aa0 100644 --- a/swarm-protocol/src/Ids.js +++ b/swarm-protocol/src/Ids.js @@ -1,5 +1,6 @@ "use strict"; const Id = require('./Id'); +const Spec = require('./Spec'); const Base64x64 = require('./Base64x64'); /** immutable id array */ @@ -84,7 +85,7 @@ class Ids { } iterator ( ) { - + return new Iterator(this); } } @@ -196,47 +197,96 @@ Ids.INC_RUN = '"'; class Iterator { - constructor (id) { - this.ids; - this.ids_offset; - this.head; - this.method; - this.body; - this.run_offset; - this.id; + /** @param {Ids} ids */ + constructor (ids) { + this.ids = ids._body; + this._m = null; + this.nextRun(); } - id () { - + get id () { + return this._id; + } + _recover_id () { + switch (this._run_type) { + case Ids.UNI_RUN: + return this._id; + case Ids.LAST2_RUN: + const two = this._run_body.substr((this._run_offset-1)<<1, 2); + return this._id = new Id(this._prefix+two, this._origin); + default: + } } next () { - + const ret = this._id; + if (this._run_offset> 1; + break; + case Ids.UNI_RUN: + this._prefix = m[1]; + this._run_length = Base64x64.base2int(this._run_body)-1; + break; + case undefined: + this._run_length = 0; + break; + default: + throw new Error('not implemented yet'); + } } - toString () { - + runMayHave (id) { + id = Id.as(id); + if (this._id===undefined) return false; + if (this._id.origin!==id.origin) return false; + if (this._run_type===Ids.UNI_RUN) + return this._id.value === id.value; + return this._prefix===id.value.substr(0,this._prefix.length); } - fromString (str, offset) { - const pos = offset || 0; - + end () { + return this._id === undefined; } - fromMatch (head, method, body) { - } + // /** @returns {Id} -- id at the pos */ + // at (offset) { + // + // } + // toString () { + // + // } + // fromString (str, offset) { TODO ToC + // const pos = offset || 0; + // + // } } +Ids.rsRun = Spec.rsQuant + + Id.rsTokExt + + '(?:([\,\'\"\;])' + + '(' + Base64x64.rs64 + '+))?'; +Ids.reRun = new RegExp(Ids.rsRun, 'g'); + Ids.Builder = Builder; Ids.Iterator = Iterator; module.exports = Ids; \ No newline at end of file diff --git a/swarm-protocol/test/07_ids.js b/swarm-protocol/test/07_ids.js index 71d98c6..e60e701 100644 --- a/swarm-protocol/test/07_ids.js +++ b/swarm-protocol/test/07_ids.js @@ -4,6 +4,26 @@ const Id = protocol.Id; const Ids = protocol.Ids; const tap = require('tape').test; +const ids1ids = [ + "ABCDEF-author", + "ABCDGH-author", + "ABCDIJ-author", + "ABCDKLM-author", + "ABCDKNO-author", + "ABCDKN-author", + "ABCDKNP-author", + "ABCDKQR-author", + "ABCDKQR-other", + // FIXME abc0 abde + Id.ZERO, + Id.ZERO, + Id.ZERO +].map(Id.as); + +const ids1str = "@ABCDEF-author'GHIJ@ABCDKLM-author'NON0NPQR@ABCDKQR-other@0;3"; + +// TODO weird cases: @00'~000~0 +// TODO invalid inputs tap ('protocol.07.A builder', function(tap) { @@ -14,22 +34,40 @@ tap ('protocol.07.A builder', function(tap) { tap.equal(b.toString(), "@0;3"); const b2 = new Ids.Builder(); - b2.append("ABCDEF-author"); - b2.append("ABCDGH-author"); - b2.append("ABCDIJ-author"); - b2.append("ABCDKLM-author"); - b2.append("ABCDKNO-author"); - b2.append("ABCDKN-author"); - b2.append("ABCDKNP-author"); - b2.append("ABCDKQR-author"); - b2.append("ABCDKQR-other"); - tap.equal(b2.toString(), - "@ABCDEF-author'GHIJ@ABCDKLM-author'NON0NPQR@ABCDKQR-other" - ); + ids1ids.forEach(i=>b2.append(i)); + tap.equal(b2.toString(), ids1str); tap.end(); }); -// tap ('protocol.07.B iterator', function(tap) { -// -// }); \ No newline at end of file +tap ('protocol.07.B iterator', function(tap) { + + const ids = new Ids(ids1str); + + const i = ids.iterator(); + + ids1ids.forEach( id => { + let next = i.next(); + tap.ok( id.eq(next) ); + } ); + + tap.ok(i.next()===undefined); + tap.ok(i.end()); + + const i2 = ids.iterator(); + let countQR = 0; + let countGH = 0; + let count0 = 0; + while (!i2.end()) { + if (i2.runMayHave("ABCDKQR-other")) countQR++; + if (i2.runMayHave("ABCDKGH-author")) countGH++; + if (i2.runMayHave(Id.ZERO)) count0++; + i2.nextRun(); + } + tap.equal(countQR, 1); + tap.equal(countGH, 2); // should be 1 + tap.equal(count0, 1); + + tap.end(); + +}); \ No newline at end of file From bf6c1050f98bfff0995da340b19b43019096e174 Mon Sep 17 00:00:00 2001 From: Victor Grishchenko Date: Fri, 16 Dec 2016 22:32:33 +0500 Subject: [PATCH 40/51] Ids.splice() - first approach --- swarm-protocol/src/Ids.js | 84 ++++++++++++++++++++++++----------- swarm-protocol/src/Ops.js | 0 swarm-protocol/test/07_ids.js | 32 ++++++++++--- 3 files changed, 85 insertions(+), 31 deletions(-) create mode 100644 swarm-protocol/src/Ops.js diff --git a/swarm-protocol/src/Ids.js b/swarm-protocol/src/Ids.js index 9c51aa0..9a2715d 100644 --- a/swarm-protocol/src/Ids.js +++ b/swarm-protocol/src/Ids.js @@ -6,11 +6,11 @@ const Base64x64 = require('./Base64x64'); /** immutable id array */ class Ids { - constructor (body) { + constructor(body) { this._body = body || ''; } - static fromString (str) { + static fromString(str) { return new Ids(str); } @@ -18,76 +18,81 @@ class Ids { return this._body; } - static as (ids) { + static as(ids) { if (!ids) return new ids(); if (ids.constructor) return ids; return new Ids(ids); } - static is (ids) { + static is(ids) { return Ids.ids_re.test(ids); } // --- access/edit API --- /** @returns {Ids} -- new array */ - splice (offset, del_count, inserts) { + splice(offset, del_count, inserts) { const b = new Builder(); - const i = this.iterator(); // append runs - while (!i.end() && i.runEndOffset()<=offset) { - body += i.runString(); + while (!i.end && i.runEndOffset < offset) { + b.appendRun(i.run); i.nextRun(); } // open split run // add that many - while (!i.end() && i.offset<=offset) { - append(i.id()); - i.next(); + while (!i.end && i.offset < offset) { + b.append(i.id); + i.nextId(); } // add new - inserts.forEach(append); + inserts.forEach(id => b.append(id)); // skip the deleted - for(let i=0; i 0) { + b.append(i.nextId()); } // append remaining runs - while (!i.end()) { - body += i.runString(); + while (!i.end) { + b.appendRun(i.run); i.nextRun(); } + + return new Ids(b.toString()); } - at (pos) { + at(pos) { // use regex scan runs // parse, .length } /** @returns {Number} -- the first position the id was found at */ - find (id) { + find(id) { } - append (id) { + append(id) { } - appendRun (run) { + appendRun(run) { } - insert (id, pos) { + insert(id, pos) { } - iterator ( ) { + iterator() { return new Iterator(this); } + [Symbol.iterator]() { + return this.iterator(); + } + } @@ -201,6 +206,7 @@ class Iterator { constructor (ids) { this.ids = ids._body; this._m = null; + this._offset = 0; this.nextRun(); } get id () { @@ -217,9 +223,16 @@ class Iterator { } } next () { + return { + value: this.nextId(), + done: this.end + }; + } + nextId () { const ret = this._id; if (this._run_offset { - let next = i.next(); + let next = i.nextId(); tap.ok( id.eq(next) ); } ); - tap.ok(i.next()===undefined); - tap.ok(i.end()); + tap.ok(i.nextId()===undefined); + tap.ok(i.end); const i2 = ids.iterator(); let countQR = 0; let countGH = 0; let count0 = 0; - while (!i2.end()) { + while (!i2.end) { if (i2.runMayHave("ABCDKQR-other")) countQR++; if (i2.runMayHave("ABCDKGH-author")) countGH++; if (i2.runMayHave(Id.ZERO)) count0++; i2.nextRun(); } tap.equal(countQR, 1); - tap.equal(countGH, 2); // should be 1 + tap.equal(countGH, 1); tap.equal(count0, 1); tap.end(); +}); + +tap ('protocol.07.C splice', function(tap) { + + const arr = ids1ids.slice(); + const ids = Ids.fromString(ids1str); + + arr.splice(7, 3, [Id.NEVER]); + const spliced = ids.splice(7, 3, [Id.NEVER]); + + tap.equal(spliced.toString(), "@ABCDEF-author'GHIJ@ABCDKLM-author'NON0NP@~'0000"); + + let i = 0; + for( var id of spliced ) { + const idstr = arr[i++].toString(); + if ( id+'' !== idstr ) + console.log(id + '?=' + idstr ); + tap.equals( id+'', idstr ); + } + + tap.end(); + }); \ No newline at end of file From f7cb8c0b972492aff0175a9b27b13104da44ce89 Mon Sep 17 00:00:00 2001 From: Victor Grishchenko Date: Sat, 17 Dec 2016 12:02:38 +0500 Subject: [PATCH 41/51] change run body separators --- swarm-protocol/src/Ids.js | 21 +++++---------------- swarm-protocol/test/07_ids.js | 6 +++--- 2 files changed, 8 insertions(+), 19 deletions(-) diff --git a/swarm-protocol/src/Ids.js b/swarm-protocol/src/Ids.js index 9a2715d..0302aa2 100644 --- a/swarm-protocol/src/Ids.js +++ b/swarm-protocol/src/Ids.js @@ -95,6 +95,11 @@ class Ids { } +Ids.UNI_RUN = ','; +Ids.LAST_RUN = "'"; +Ids.LAST2_RUN = '"'; +Ids.INC_RUN = '`'; + class Builder { @@ -195,11 +200,6 @@ class Builder { } } -Ids.UNI_RUN = ';'; -Ids.LAST_RUN = ','; -Ids.LAST2_RUN = "'"; -Ids.INC_RUN = '"'; - class Iterator { /** @param {Ids} ids */ @@ -300,17 +300,6 @@ class Iterator { return this._id === undefined; } - // /** @returns {Id} -- id at the pos */ - // at (offset) { - // - // } - // toString () { - // - // } - // fromString (str, offset) { TODO ToC - // const pos = offset || 0; - // - // } } Ids.rsRun = Spec.rsQuant + diff --git a/swarm-protocol/test/07_ids.js b/swarm-protocol/test/07_ids.js index 990c724..06d4c4a 100644 --- a/swarm-protocol/test/07_ids.js +++ b/swarm-protocol/test/07_ids.js @@ -20,7 +20,7 @@ const ids1ids = [ Id.ZERO ].map(Id.as); -const ids1str = "@ABCDEF-author'GHIJ@ABCDKLM-author'NON0NPQR@ABCDKQR-other@0;3"; +const ids1str = '@ABCDEF-author"GHIJ@ABCDKLM-author"NON0NPQR@ABCDKQR-other@0,3'; // TODO weird cases: @00'~000~0 // TODO invalid inputs @@ -31,7 +31,7 @@ tap ('protocol.07.A builder', function(tap) { b.append(Id.ZERO); b.append(Id.ZERO); b.append(Id.ZERO); - tap.equal(b.toString(), "@0;3"); + tap.equal(b.toString(), "@0,3"); const b2 = new Ids.Builder(); ids1ids.forEach(i=>b2.append(i)); @@ -80,7 +80,7 @@ tap ('protocol.07.C splice', function(tap) { arr.splice(7, 3, [Id.NEVER]); const spliced = ids.splice(7, 3, [Id.NEVER]); - tap.equal(spliced.toString(), "@ABCDEF-author'GHIJ@ABCDKLM-author'NON0NP@~'0000"); + tap.equal(spliced.toString(), '@ABCDEF-author"GHIJ@ABCDKLM-author"NON0NP@~"0000'); let i = 0; for( var id of spliced ) { From 19a76aea7205b3391f876bb14e7a7f27712bb63d Mon Sep 17 00:00:00 2001 From: Victor Grishchenko Date: Mon, 19 Dec 2016 19:51:38 +0500 Subject: [PATCH 42/51] naive Ids API --- swarm-protocol/src/Base64x64.js | 10 ++++++++++ swarm-protocol/src/Id.js | 2 +- swarm-protocol/src/Ids.js | 33 +++++++++++++++++++++------------ swarm-protocol/src/Spec.js | 1 + swarm-protocol/test/07_ids.js | 15 ++++++++++++++- 5 files changed, 47 insertions(+), 14 deletions(-) diff --git a/swarm-protocol/src/Base64x64.js b/swarm-protocol/src/Base64x64.js index 8160277..aca603e 100644 --- a/swarm-protocol/src/Base64x64.js +++ b/swarm-protocol/src/Base64x64.js @@ -309,6 +309,15 @@ class Base64x64 { return new Base64x64(base).round(pos).toString(); } + /** @returns {String} - not a valid Base64x64 number */ + static commonPrefix (one, two) { + let common = ''; + const v1 = one.toString(), v2 = two.toString(); + for(let i=0; i1 && liv.length>1 && + iv.length<=prefix.length+2 && liv.length<=prefix.length+2) { this.runtype = Ids.LAST2_RUN; this.prefixlen = prefix.length; this.runlen = 1; diff --git a/swarm-protocol/src/Spec.js b/swarm-protocol/src/Spec.js index e72c8cf..e512be5 100644 --- a/swarm-protocol/src/Spec.js +++ b/swarm-protocol/src/Spec.js @@ -252,6 +252,7 @@ class Spec { } Spec.quants = "#.@:"; +Spec.rsQuant = '[' + Spec.quants.replace(/./g, '\\$&') +']'; Spec.rsSpec = Spec.quants.replace(/./g, '(?:\\$&('+Id.rsTok+'))?'); Spec.reSpec = new RegExp('^'+Spec.rsSpec+'$', 'g'); Spec.NON_SPECIFIC_NOOP = new Spec(); diff --git a/swarm-protocol/test/07_ids.js b/swarm-protocol/test/07_ids.js index 06d4c4a..4aa4642 100644 --- a/swarm-protocol/test/07_ids.js +++ b/swarm-protocol/test/07_ids.js @@ -78,9 +78,13 @@ tap ('protocol.07.C splice', function(tap) { const ids = Ids.fromString(ids1str); arr.splice(7, 3, [Id.NEVER]); + tap.equal (ids.length, 12, 'length'); const spliced = ids.splice(7, 3, [Id.NEVER]); - tap.equal(spliced.toString(), '@ABCDEF-author"GHIJ@ABCDKLM-author"NON0NP@~"0000'); + tap.equal(spliced.toString(), + '@ABCDEF-author"GHIJ@ABCDKLM-author"NON0NP@~@0,2'); + + tap.equal (spliced.length, 10, 'spliced length'); let i = 0; for( var id of spliced ) { @@ -90,6 +94,15 @@ tap ('protocol.07.C splice', function(tap) { tap.equals( id+'', idstr ); } + for(let k=0; k Date: Mon, 26 Dec 2016 08:27:18 +0500 Subject: [PATCH 43/51] simplified OpStream goes to swarm-protocol --- .../src/Client.js | 0 swarm-protocol/src/OpStream.js | 259 ++++++++++++ {swarm-syncable => swarm-protocol}/src/URL.js | 0 swarm-protocol/test/09_opstream.js | 120 ++++++ swarm-syncable/src/OpStream.js | 377 ------------------ swarm-syncable/test/00_OpStream.js | 175 -------- 6 files changed, 379 insertions(+), 552 deletions(-) rename {swarm-syncable => swarm-protocol}/src/Client.js (100%) create mode 100644 swarm-protocol/src/OpStream.js rename {swarm-syncable => swarm-protocol}/src/URL.js (100%) create mode 100644 swarm-protocol/test/09_opstream.js delete mode 100644 swarm-syncable/src/OpStream.js delete mode 100644 swarm-syncable/test/00_OpStream.js diff --git a/swarm-syncable/src/Client.js b/swarm-protocol/src/Client.js similarity index 100% rename from swarm-syncable/src/Client.js rename to swarm-protocol/src/Client.js diff --git a/swarm-protocol/src/OpStream.js b/swarm-protocol/src/OpStream.js new file mode 100644 index 0000000..de3347d --- /dev/null +++ b/swarm-protocol/src/OpStream.js @@ -0,0 +1,259 @@ +"use strict"; +const swarm = require('swarm-protocol'); +const Base64x64 = require('./Base64x64'); +const Id = require('./Id'); +const Spec = swarm.Spec; +const Op = swarm.Op; +const URL = require('./URL'); + +/** + * + * */ +class OpStream { + + constructor (options) { + this._lstn = []; + /** db replica id: dbid-replicaid */ + this._debug = (options && options.debug) ? + (options.debug===true?this.constructor.name[0]:options.debug) : null; + } + + _on_upstream () { + + } + + /** internal callback; triggered on a first listener added iff nothing + * has been emitted yet. */ + _on_downstream () { + + } + + /** add a new listener + * @param {OpStream} opstream - the downstream + */ + on (opstream) { + if (opstream.constructor===Function) + opstream = new CallbackOpStream(opstream); + if (!opstream._emitted || opstream._emitted.constructor!==Function) + throw new Error('opstreams only'); + this._lstn.push(opstream); + return opstream; + } + + off (opstream) { + if (!opstream._emitted) + throw new Error("can only add/remove opstreams"); + if (!this._lstn) return; + const i = this._lstn.indexOf(opstream); + i!==-1 && this._lstn.splice(i, 1); + } + + once (callback) { + const opstream = new CallbackOpStream(callback, true); + return this.on(opstream); + } + + onId (id, opstream) { + const i = Id.as(id); + return this.on( new FilterOpStream(o=>o&&o.Id.eq(i), opstream) ); + } + + onHandshake (opstream) { + return this.on( new FilterOpStream(o=>o&&o.isHandshake(), opstream) ); + } + + onOnOff (opstream) { + return this.on( new FilterOpStream(o=>o&&o.isOnOff(), opstream) ); + } + + onMutation (opstream) { + return this.on( new FilterOpStream(o=>o&&!o.isAbnormal(), opstream) ); + } + + onType (id, opstream) { + const t = Id.as(id); + return this.on( new FilterOpStream(o=>o&&o.Type.eq(t), opstream) ); + } + + onOrigin (rid, opstream) { + return this.on( new FilterOpStream(o=>o&&o.origin==rid, opstream) ); + } + + onEvent (name, opstream) { + return this.on( new FilterOpStream( o=>o&&o.eventName==name, opstream ) ); + } + + onceEvent (name, opstream) { + return this.on( new FilterOpStream( o=>o&&o.eventName==name, opstream, true ) ); + } + + /** Emit a new op to all the interested listeners. + * @param {Op} op - the op to emit, null for EOF */ + emit (op) { + if (this._debug) + console.warn('{'+this._debug+'\t'+(op?op.toString():'[EOF]')); + let ejects = [], l = this._lstn; + for(let i=0; i this.off(e) ); + if (op===null) + this._lstn = null; + } + + commit (op) { + + } + + /** @param {Op} op - emitted op or null for EOF */ + _emitted (op) { + if (this._debug) + console.warn('{'+this._debug+'\t'+op.toString()); + return this.emit(op); + } + + /** @param {Op} op - committed op or null for source EOF + * @param {OpStream} source - downstream op source */ + _committed (op, source) { + if (this._debug) + console.warn('}'+this._debug+'\t'+op.toString()); + /** by default, an echo stream */ + this.commit(op); + } + + emitAll (ops) { + ops.forEach(op => this.emit(op)); + } + + + commitAll (ops) { + ops.forEach(op => this.commit(op)); + } + + end () { + this.commit(null); + } + + onceEnd (callback) { + this.on( new FilterOpStream( o => o==null, new CallbackOpStream(callback) ) ); + } + + static connect (url, options) { + if (url.constructor!==URL) + url = new URL(url.toString()); + const top_proto = url.scheme[0]; + const fn = OpStream._URL_HANDLERS[top_proto]; + if (!fn) + throw new Error('unknown protocol: '+top_proto); + return new fn(url, options||Object.create(null)); + } + + static listen (url, options, upstream) { + if (url.constructor!==URL) + url = new URL(url.toString()); + const top_proto = url.scheme[0]; + const fn = OpStream._SERVER_URL_HANDLERS[top_proto]; + if (!fn) + throw new Error('unknown protocol: '+top_proto); + return new fn(url, options||Object.create(null), upstream); + } + +} + +OpStream.MUTATIONS = "^.on.off.error.~"; +OpStream.HANDSHAKES = ".on.off"; +OpStream.STATES = ".~"; +OpStream.ENOUGH = Symbol('enough'); +OpStream.OK = Symbol('ok'); +OpStream.SLOW_DOWN = Symbol('slow'); // TODO relay backpressure +OpStream._URL_HANDLERS = Object.create(null); +OpStream._SERVER_URL_HANDLERS = Object.create(null); +module.exports = OpStream; + +/** a test op stream */ +class ZeroOpStream extends OpStream { + + constructor (url, options) { + super(); + if (url) { + this.url = new URL(url); + const host = this.url.host; + if (OpStream.QUEUES[host]) { + return OpStream.QUEUES[host]; + } else { + OpStream.QUEUES[host] = this; + } + } else { + this.url = null; + } + this.ops = this.committed = []; + this.emitted = []; + } + + _committed (op) { + this.committed.push(op); + } + + _emitted (op) { + this.emitted.push(op); + } + +} +OpStream.QUEUES = Object.create(null); +OpStream._URL_HANDLERS['0'] = ZeroOpStream; +OpStream.ZeroOpStream = ZeroOpStream; + + +class CallbackOpStream extends OpStream { + + constructor (callback, once) { + super(); + if (!callback || callback.constructor!==Function) + throw new Error('callback is not a function'); + this._callback = callback; + this._once = !!once; + this._in = false; + } + + emit (op) { + if (this._in) return; + this._in = true; + const enough = this._callback(op)===OpStream.ENOUGH; + this._in = false; // FIXME + return (enough || this._once) ? + OpStream.ENOUGH : OpStream.OK; + } + +} + + +class FilterOpStream extends OpStream { + + constructor (filter_fn, downstream, once) { + super(); + this._filter = filter_fn; + this._once = once || false; + this._fn = null; + if (downstream) { + if (downstream._emitted) + this.on(downstream); + else + this._fn = downstream; + } + } + + _emitted (op) { + if (this._filter(op)) { + this.emit(op); + this._fn && this._fn(op); + } + return this._once || this._lstn===null ? OpStream.ENOUGH : OpStream.OK; + } + +} + +FilterOpStream.rsTok = '([/#!\\.])(' + swarm.Stamp.rsTok + ')'; +FilterOpStream.reTok = new RegExp(FilterOpStream.rsTok, 'g'); +OpStream.Filter = FilterOpStream; diff --git a/swarm-syncable/src/URL.js b/swarm-protocol/src/URL.js similarity index 100% rename from swarm-syncable/src/URL.js rename to swarm-protocol/src/URL.js diff --git a/swarm-protocol/test/09_opstream.js b/swarm-protocol/test/09_opstream.js new file mode 100644 index 0000000..825a910 --- /dev/null +++ b/swarm-protocol/test/09_opstream.js @@ -0,0 +1,120 @@ +"use strict"; +let tape = require('tape').test; +let swarm = require('swarm-protocol'); +let OpStream = require('../src/OpStream'); +const Spec = swarm.Spec; +let Op = swarm.Op; + +tape ('syncable.00.A echo op stream - event filtering', function (t) { + + // OpStream is a semi-abstract base class, an echo stream + let stream = new OpStream(); + // ops to emit + let ops = Op.parseFrame( + "#test.db@time:~off\n" + + "#7AM0f+gritzko.json@0:~on\n" + + "#7AM0f+gritzko.json@7AM0f12+origin:num=1\n" + + '#7AM0f+gritzko.json@7AM0f+gritzko:key="value"\n' + ); + + let ons = 0, onoffs = 0; + let origin = 0, mutationsB = 0, myobj = 0; + let unsub = 0; + + // OK, let's play with filters and listeners + // the syntax for the filters is the same as for Specs, + // except each token may have many accepted values (OR) + // Different tokens are AND'ed. + + stream.onEvent(Op.ON_OP_NAME, op => ons++); + + stream.onOrigin("origin", op => origin++); + + // may use stream.onHandshake(op => onoffs++) + // means: ".on OR .off" + stream.onOnOff(op => onoffs++); + + // the leading ^ is a negation, i.e "NOT (.on OR .off OR ...)" + //stream.ontch("^.on.off.error.~", op=> mutationsA++); + // exactly the same result, without the mumbo-jumbo: + stream.onMutation (op => mutationsB++); + + // filters database close event, ".off AND /Swarm" + stream.onHandshake().onType("db", op=> unsub++); + + // this catches a fresh subscription to #7AM0f+gritzko + stream.onId("7AM0f+gritzko").onType("json", on => { + myobj++; + }); + + // may use stream.on ( null, () => {...} ) + stream.onceEnd(nothing => { + t.equals(nothing, null, 'null'); + t.equals(ons, 1, 'onEvent'); + t.equals(onoffs, 2, 'onOnOff'); + t.equals(origin, 1, 'onOrigin'); + t.equals(mutationsB, 2, 'onMutation'); + t.equals(myobj, 3, 'onId().onType()'); + t.equals(unsub, 1, 'onHandshake.onType'); + t.end(); + }); + + // feed all the ops into the echo stream to trigger listeners + ops.forEach( o => stream._emitted(o) ); + // stream.offer(null) has the same effect as stream.end() + stream._emitted(null); + +}); + + +tape ('syncable.00.A echo op stream - listener mgmt', function (t) { + + let ops = Op.parseFrame (':~on\n:~off\n#test.db:~on="value"\n:~off\n'); + + let stream = new OpStream(); + + let once = 0, ons = 0, first_on = false; + let total = 0, total2 = 0, before_value = 0, three=0; + + stream.on(op => total++); + stream.on(op => total2++); + stream.onceEvent(Spec.ON_OP_NAME, op => once++ ); + stream.onEvent(Spec.ON_OP_NAME, () => ons++ ); + stream.onEvent(Spec.ON_OP_NAME, op => { + first_on = true; + }); + stream.on( op => { + if (op && op.value) + return OpStream.ENOUGH; + before_value++; + }); + const handle = stream.on(function removable (op) { + three++; + if (op.type=='db') + stream.off(handle); + }); + + stream.emitAll(ops); + stream.emit(null); + + t.equals(once, 1, 'once'); + t.equals(ons, 2); + t.equals(total, 5, 'total'); + t.equals(total2, 5, 'total(2)'); + t.equals(before_value, 2); + t.equals(three, 3); + t.ok(first_on); + + t.end(); + +}); + + +tape ('syncable.00.D op stream URL', function (t) { + const zero = OpStream.connect('0://00.D'); + t.ok(zero===OpStream.QUEUES['00.D']); + zero._committed(Op.NON_SPECIFIC_NOOP); + t.equals(zero.ops.length, 1); + t.ok(zero.ops[0]===Op.NON_SPECIFIC_NOOP); + t.end(); +}); diff --git a/swarm-syncable/src/OpStream.js b/swarm-syncable/src/OpStream.js deleted file mode 100644 index 50bf598..0000000 --- a/swarm-syncable/src/OpStream.js +++ /dev/null @@ -1,377 +0,0 @@ -"use strict"; -const swarm = require('swarm-protocol'); -const Spec = swarm.Spec; -const Op = swarm.Op; -const URL = require('./URL'); - -const MUTE=0, ONE_LSTN=1, MANY_LSTN=2, PENDING=3; - -/** - * - * */ -class OpStream { - - constructor (options) { - this._lstn = null; - /** db replica id: dbname+replica */ - this._dbrid = null; - this._debug = (options && options.debug) ? - (options.debug===true?this.constructor.name[0]:options.debug) : null; - this.error_message = null; - } - - get replicaId () { - return this._dbrid && this._dbrid.origin; - } - - get dbId () { - return this._dbrid && this._dbrid.value; - } - - get dbrid () { - return this._dbrid.toString(); - } - - _lstn_state () { - if (this._lstn===null) - return MUTE; - else if (this._lstn._apply) - return ONE_LSTN; - else if (this._lstn.length===0 || this._lstn[0].constructor===Op) - return PENDING; - else if (this._lstn[0]._apply) - return MANY_LSTN; - else - throw new Error('invalid _lstn'); - } - - /** add a new listener - * @param {OpStream} opstream - the downstream - */ - on (opstream) { - if (opstream.constructor===Function) - opstream = new CallbackOpStream(opstream); - if (!opstream._apply || opstream._apply.constructor!==Function) - throw new Error('opstreams only'); - switch (this._lstn_state()) { - case MUTE: - this._lstn = opstream; - break; - case ONE_LSTN: - this._lstn = [this._lstn, opstream]; - break; - case MANY_LSTN: - this._lstn.push(opstream); - break; - case PENDING: - const ops = this._lstn; - this._lstn = opstream; - this._emitAll(ops); - break; - } - return opstream; - } - - onMatch (filter, opstream) { - if (opstream.constructor===Function) - opstream = new CallbackOpStream(opstream); - const fs = new FilterOpStream(filter); - fs.on(opstream); - return this.on(fs); - } - - onceMatch (filter, callback) { - const fs = new FilterOpStream(filter); - const opstream = new CallbackOpStream(callback, true); - fs.on(opstream); - return this.on(fs); - } - - once (callback) { - const opstream = new CallbackOpStream(callback, true); - return this.on(opstream); - } - - /** internal callback; triggered on a first listener added iff nothing - * has been emitted yet. */ - _start () {} - - /** remove listener(s) */ - off (opstream) { - if (!opstream._apply) - throw new Error("can only add/remove opstreams"); - switch (this._lstn_state()) { - case MUTE: - break; - case ONE_LSTN: - if (this._lstn === opstream) - this._lstn = null; - break; - case MANY_LSTN: - const i = this._lstn.indexOf(opstream); - if (i!==-1) - this._lstn.splice(i, 1); - if (this._lstn.length===0) - this._lstn = null; - break; - case PENDING: - break; - } - return opstream; - } - - /** Emit a new op to all the interested listeners. - * If nobody listens yet, the op is queued to be delivered to the first - * listener. Call opstream.spill() to stop queueing. - * @param {Op} op - the op to emit */ - _emit (op) { - if (this._debug) - console.warn('{'+this._debug+'\t'+(op?op.toString():'[EOF]')); - switch (this._lstn_state()) { - case MUTE: - break; - case ONE_LSTN: - if (this._lstn._apply(op)===OpStream.ENOUGH) - this._lstn = null; - break; - case MANY_LSTN: - let ejects = [], l = this._lstn; - for(let i=0; i this.off(e) ); - break; - case PENDING: - this._lstn.push(op); - break; - } - // this._emit(null) removes all listeners to prevent further emits - if (op===null) - this._lstn = null; - } - - _error (message) { - this.error_message = message; - this._emit(null); - } - - _emitAll (ops) { - ops.forEach(op => this._emit(op)); - } - - _apply (op, upstream) { - } - - pollAll () { - let ret = null; - if (this._lstn_state()===PENDING) { - ret = this._lstn; - this._lstn = null; - } - return ret; - } - - poll () { - if (this._lstn_state()===PENDING) - return this._lstn.shift(); - else - return null; - } - - /** by default, an echo stream */ - offer (op, downstream) { - if (this._debug) - console.warn('}'+this._debug+'\t'+op.toString()); - this._emit(op); - } - - offerAll (ops) { - ops.forEach(op => this.offer(op)); - } - - end () { - this.offer(null); - } - - onceEnd (callback) { - this.onceMatch(null, callback); - } - - onHandshake (callback) { - this.onMatch(OpStream.HANDSHAKES, callback); - } - - onMutation (callback) { - this.onMatch(OpStream.MUTATIONS, callback); - } - - onState (callback) { - this.onMatch(OpStream.STATES, callback); - } - - /* - _listFilters () { - if (!this._filters) return ''; - let list = this._up ? '*\t' + this._up.toString() : ''; - list += this._filters.map(f => - f.toString()+'\t'+f.callback.toString() - ).join('\n'); - return list; - } - */ - - static connect (url, options) { - if (url.constructor!==URL) - url = new URL(url.toString()); - const top_proto = url.scheme[0]; - const fn = OpStream._URL_HANDLERS[top_proto]; - if (!fn) - throw new Error('unknown protocol: '+top_proto); - return new fn(url, options||Object.create(null)); - } - - static listen (url, options, upstream) { - if (url.constructor!==URL) - url = new URL(url.toString()); - const top_proto = url.scheme[0]; - const fn = OpStream._SERVER_URL_HANDLERS[top_proto]; - if (!fn) - throw new Error('unknown protocol: '+top_proto); - return new fn(url, options||Object.create(null), upstream); - } - -} - -OpStream.MUTATIONS = "^.on.off.error.~"; -OpStream.HANDSHAKES = ".on.off"; -OpStream.STATES = ".~"; -OpStream.ENOUGH = Symbol('enough'); -OpStream.OK = Symbol('ok'); -OpStream.SLOW_DOWN = Symbol('slow'); // TODO relay backpressure -OpStream._URL_HANDLERS = Object.create(null); -OpStream._SERVER_URL_HANDLERS = Object.create(null); -module.exports = OpStream; - -/** a test op stream */ -class ZeroOpStream extends OpStream { - - constructor (url, options) { - super(); - if (url) { - this.url = new URL(url); - const host = this.url.host; - if (OpStream.QUEUES[host]) { - return OpStream.QUEUES[host]; - } else { - OpStream.QUEUES[host] = this; - } - } else { - this.url = null; - } - this.ops = this.offered = []; - this.applied = []; - } - - offer (op) { - this.ops.push(op); - } - - _apply (op) { - this.applied.push(op); - } - -} -OpStream.QUEUES = Object.create(null); -OpStream._URL_HANDLERS['0'] = ZeroOpStream; -OpStream.ZeroOpStream = ZeroOpStream; - - -class CallbackOpStream extends OpStream { - - constructor (callback, once) { - super(); - if (!callback || callback.constructor!==Function) - throw new Error('callback is not a function'); - this._callback = callback; - this._once = !!once; - this._in = false; - } - - _apply (op) { - if (this._in) return; - this._in = true; - const enough = this._callback(op)===OpStream.ENOUGH; - this._in = false; // FIXME - return (enough || this._once) ? - OpStream.ENOUGH : OpStream.OK; - } - -} - - -class FilterOpStream extends OpStream { - - constructor (string, once) { - super(); - this._negative = string && string.charAt(0)==='^'; - this._patterns = [null, null, null, null]; - this._once = once; - if (string===null) { //eof - this._patterns = null; - return; - } - let m = null; - FilterOpStream.reTok.lastIndex = this._negative ? 1 : 0; - while (m = FilterOpStream.reTok.exec(string)) { - let quant = m[1], stamp = m[2], t = Spec.quants.indexOf(quant); - if (this._patterns[t]===null) { - this._patterns[t] = []; - } - this._patterns[t].push(new swarm.Stamp(stamp)); - } - } - - matches (op) { - let pns = this._patterns; - if (op===null || pns===null) { - return op===null && (pns===null || pns.every(p=>p===null)); - } - let spec = op.spec; - for(let t=0; t<4; t++) { - let mine = pns[t]; - if (mine===null) continue; - let its = spec._toks[t]; - let bad = mine.every(stamp => !stamp.eq(its)); - if (bad) return this._negative; - } - return !this._negative; - } - - _offer () { - throw new Error('not implemented'); - } - - _apply (op, opstream) { - if (this.matches(op)) - this._emit(op); - return this._once || this._lstn===null ? OpStream.ENOUGH : OpStream.OK; - } - - toString () { - let p = this._patterns; - if (p===null) return null; // take that (TODO) - let ret = this._negative ? '^' : ''; - for(let q=0; q<4; q++) - if (p[q]!==null) { - p[q].forEach(stamp => ret+=Spec.quants[q]+stamp); - } - return ret; - } - -} - -FilterOpStream.rsTok = '([/#!\\.])(' + swarm.Stamp.rsTok + ')'; -FilterOpStream.reTok = new RegExp(FilterOpStream.rsTok, 'g'); -OpStream.Filter = FilterOpStream; diff --git a/swarm-syncable/test/00_OpStream.js b/swarm-syncable/test/00_OpStream.js deleted file mode 100644 index 0c78a01..0000000 --- a/swarm-syncable/test/00_OpStream.js +++ /dev/null @@ -1,175 +0,0 @@ -"use strict"; -let tape = require('tape').test; -let swarm = require('swarm-protocol'); -let OpStream = require('../src/OpStream'); -let Op = swarm.Op; - -tape ('syncable.00.A echo op stream - event filtering', function (t) { - - // OpStream is a semi-abstract base class, an echo stream - let stream = new OpStream(); - // ops to emit - let ops = Op.parseFrame( - "/Swarm#test!time.off\n" + - "/Object#7AM0f+gritzko!0.on\n" + - "/Object#7AM0f+gritzko!7AM0f+gritzko.~\n" + - "/Object#7AM0f+gritzko!7AM0f+gritzko.key\tvalue\n" - ); - - let ons = 0, onoffs = 0, states = 0; - let mutationsA = 0, mutationsB = 0, myobj = 0; - let unsub = false; - - // OK, let's play with filters and listeners - // the syntax for the filters is the same as for Specs, - // except each token may have many accepted values (OR) - // Different tokens are AND'ed. - - stream.onMatch(".on", op => ons++); - - // may use stream.onHandshake(op => onoffs++) - // means: ".on OR .off" - stream.onMatch(".on.off", op => onoffs++); - - // may use stream.on(".~", op => states++) - stream.onState(op => states++); - - // the leading ^ is a negation, i.e "NOT (.on OR .off OR ...)" - stream.onMatch("^.on.off.error.~", op=> mutationsA++); - // exactly the same result, without the mumbo-jumbo: - stream.onMutation(op=> mutationsB++); - - // filters database close event, ".off AND /Swarm" - stream.onMatch("/Swarm.off", op=> unsub=true); - - // this catches a fresh subscription to #7AM0f+gritzko - stream.onMatch("/Object#7AM0f+gritzko!0", on => { - myobj++; - t.ok(on.isOn()); - }); - - // may use stream.on ( null, () => {...} ) - stream.onceEnd(nothing => { - t.equals(nothing, null); - t.equals(ons, 1); - t.equals(onoffs, 2); - t.equals(mutationsA, 1); - t.equals(mutationsB, 1); - t.equals(myobj, 1); - t.ok(unsub); - t.end(); - }); - - // feed all the ops into the echo stream to trigger listeners - stream.offerAll(ops); - // stream.offer(null) has the same effect as stream.end() - stream.offer(null); - - // in case you'll need to debug that, v8 is awesome: - // console.log(stream._listFilters()); - // - // .on op => ons++ - // .on.off op => onoffs++ - // .~ op => states++ - // ^.on.off.error.~ op=> mutationsA++ - // ^.on.off.error.~ op=> mutationsB++ - // /Swarm.off op=> unsub=true - // /Object#7AM0f+gritzko!0 on => { - // myobj++; - // t.ok(on.isOn()); - // } - // null nothing => { - // t.equals(nothing, null); - // t.equals(ons, 1); - // t.equals(onoffs, 2); - // t.equals(mutationsA, 1); - // t.equals(mutationsB, 1); - // t.equals(myobj, 1); - // t.ok(unsub); - // t.end(); - // } - -}); - - -tape ('syncable.00.A echo op stream - listener mgmt', function (t) { - - let ops = Op.parseFrame (".on\n.off\n/Swarm.on\tvalue\n.off\n"); - - let stream = new OpStream(); - - let once = 0, ons = 0, first_on = false; - let total = 0, total2 = 0, before_value = 0, three=0; - - stream.on(op => total++); - stream.on(op => total2++); - stream.onceMatch('.on', op => once++ ); - stream.onMatch('.on', () => ons++ ); - stream.onMatch('.on', op => { - first_on = true; - }); - stream.on( op => { - if (op && op.value) - return OpStream.ENOUGH; - before_value++; - }); - const handle = stream.on(function removable (op) { - three++; - if (op.type=='Swarm') - stream.off(handle); - }); - - stream.offerAll(ops); - stream.end(); - - t.equals(once, 1); - t.equals(ons, 2); - t.equals(total, 5); - t.equals(total2, 5); - t.equals(before_value, 2); - t.equals(three, 3); - t.ok(first_on); - - t.end(); - -}); - -tape ('syncable.00.B op stream - filter', function (t) { - - let filter = '^/Swarm.off'; - - let f = new OpStream.Filter(filter, t.end); - - t.equals(f.toString(), filter); - - t.end(); - //let ops = Op.parseFrame ("/Swarm.off\n/Swarm.on\n"); - -}); - -tape ('syncable.00.C op stream - queue', function (t) { - let stream = new OpStream(); - stream._lstn = []; - let count = 0, tail = 0; - stream.offer(Op.NOTHING); - stream.offer(Op.NOTHING); - stream.on(op => count++); - stream.on(op => tail++); - stream.offer(Op.NOTHING); - t.equals(count, 3); - t.equals(tail, 1); - stream._lstn = null; // only pool ops if lstn is [] - stream.offer(Op.NOTHING); - t.equals(count, 3); - t.equals(tail, 1); - t.end(); -}); - -tape ('syncable.00.D op stream URL', function (t) { - const zero = OpStream.connect('0://00.D'); - t.ok(zero===OpStream.QUEUES['00.D']); - zero.offer(Op.NON_SPECIFIC_NOOP); - t.equals(zero.ops.length, 1); - t.ok(zero.ops[0]===Op.NON_SPECIFIC_NOOP); - t.end(); -}); From 9951e75519079c3327f05a0fc25ca363bdad8f82 Mon Sep 17 00:00:00 2001 From: Victor Grishchenko Date: Wed, 28 Dec 2016 11:48:55 +0500 Subject: [PATCH 44/51] iterator bugfix --- swarm-protocol/src/Ids.js | 30 +++++++++++++++---- swarm-protocol/test/07_ids.js | 14 +++++++++ .../test/0A_Client.js | 0 3 files changed, 38 insertions(+), 6 deletions(-) rename swarm-syncable/test/02_Client.js => swarm-protocol/test/0A_Client.js (100%) diff --git a/swarm-protocol/src/Ids.js b/swarm-protocol/src/Ids.js index 26bf13e..4337b77 100644 --- a/swarm-protocol/src/Ids.js +++ b/swarm-protocol/src/Ids.js @@ -20,8 +20,10 @@ class Ids { } static as(ids) { - if (!ids) return new ids(); - if (ids.constructor) return ids; + if (!ids) + return new Ids(); + if (ids.constructor===Ids) + return ids; return new Ids(ids); } @@ -76,8 +78,9 @@ class Ids { find(id) { const seek = Id.as(id); const i = this.iterator(); - while (!i.end && !seek.eq(i.nextId())); // FIXME span skip - return i.end ? -1 : i.offset-1; // FIXME + while (!i.end && !seek.eq(i.id)) + i.next(); // FIXME span skip + return i.end ? -1 : i.offset; // FIXME } _runScan () { @@ -101,6 +104,19 @@ class Ids { return this.iterator(); } + static fromIdArray (id_array) { + const b = new Builder(); + id_array.forEach( id => b.append(id) ); + return new Ids(b.toString()); + } + + toArray () { + const ret = []; + for(let id of this) + ret.push(id); + return ret; + } + } Ids.UNI_RUN = ','; @@ -232,10 +248,12 @@ class Iterator { } } next () { - return { - value: this.nextId(), + const ret = { + value: this._id, done: this.end }; + this.nextId(); + return ret; } nextId () { const ret = this._id; diff --git a/swarm-protocol/test/07_ids.js b/swarm-protocol/test/07_ids.js index 4aa4642..fc80af2 100644 --- a/swarm-protocol/test/07_ids.js +++ b/swarm-protocol/test/07_ids.js @@ -105,4 +105,18 @@ tap ('protocol.07.C splice', function(tap) { tap.end(); +}); + +tap('protocol.08.D corner cases', function (tap) { + + const tailing_zeros = '@stamp00-author"102003'; + const tz_ids = Ids.fromString(tailing_zeros); + const tz_array = tz_ids.toArray(); + tap.equal(tz_array[0]+'', 'stamp-author'); + tap.equal(tz_array[1]+'', 'stamp1-author'); + tap.equal(tz_array[2]+'', 'stamp2-author'); + tap.equal(tz_array[3]+'', 'stamp03-author'); + + tap.end(); + }); \ No newline at end of file diff --git a/swarm-syncable/test/02_Client.js b/swarm-protocol/test/0A_Client.js similarity index 100% rename from swarm-syncable/test/02_Client.js rename to swarm-protocol/test/0A_Client.js From 29bed170d343026474128954cb5d399ab515a29c Mon Sep 17 00:00:00 2001 From: Victor Grishchenko Date: Thu, 29 Dec 2016 15:02:04 +0500 Subject: [PATCH 45/51] mature the Ids implementation --- swarm-protocol/src/Ids.js | 408 +++++++++++++++++++++------------- swarm-protocol/test/07_ids.js | 31 ++- 2 files changed, 270 insertions(+), 169 deletions(-) diff --git a/swarm-protocol/src/Ids.js b/swarm-protocol/src/Ids.js index 4337b77..4db27a4 100644 --- a/swarm-protocol/src/Ids.js +++ b/swarm-protocol/src/Ids.js @@ -38,8 +38,8 @@ class Ids { const b = new Builder(); const i = this.iterator(); // append runs - while (!i.end && i.runEndOffset < offset) { - b.appendRun(i.run); + while (!i.end && i.run_end_offset < offset) { + b.appendRun(i.open_run); i.nextRun(); } // open split run @@ -54,12 +54,12 @@ class Ids { for (let j = 0; j < del_count && !i.end; j++) i.nextId(); // add the rest - while (!i.end && i.runOffset > 0) { + while (!i.end && i.run_offset > 0) { b.append(i.nextId()); } // append remaining runs while (!i.end) { - b.appendRun(i.run); + b.appendRun(i.open_run); i.nextRun(); } @@ -120,108 +120,245 @@ class Ids { } Ids.UNI_RUN = ','; -Ids.LAST_RUN = "'"; +Ids.LAST1_RUN = "'"; Ids.LAST2_RUN = '"'; Ids.INC_RUN = '`'; +/** A run of a single id. */ +class IdRun { + constructor (id) { + this.id = Id.as(id); + } + append (new_id) { + const id = Id.as(new_id); + if (this.id.eq(id)) + return new UniRun(this.id).append(id); + if (!this.id.isSameOrigin(id)) + return null; + if (this.id.value.length===1) + return null; + const prefix = Base64x64.commonPrefix(this.id.value, id.value); + if (prefix.length+1>=this.id.value.length && prefix.length+1>=id.value.length) + return new Last1Run(this.id).append(id); + if (prefix.length+2>=this.id.value.length && prefix.length+2>=id.value.length) + return new Last2Run(this.id).append(id); + return null; + } + at (i) { + return i===0 ? this.id : undefined; + } + get length () { + return 1; + } + toString () { + return this.id.toString(); + } + /** @returns {Boolean} */ + mayHave (id) { + return this.id.eq(id); + } + static fromString (str) { + Ids.reRun.lastIndex = 0; + const m = Ids.reRun.exec(str); + return m ? IdRun.fromMatch(m) : null; + } + static fromMatch (m) { + const value = m[1], origin = m[2], run_type = m[3], tail = m[4]; + const id = new Id(value, origin), len = value.length; + switch (run_type) { + case Ids.UNI_RUN: return new UniRun(id, tail); + case Ids.LAST1_RUN: return new Last1Run(id, value.substr(0,len-1), tail); + case Ids.LAST2_RUN: return new Last2Run(id, value.substr(0,len-2), tail); + case undefined: return new IdRun(id); + default: throw new Error('parsing fail'); + } + } + static as (run_or_string) { + if (run_or_string instanceof IdRun) + return run_or_string; -class Builder { + } +} - constructor () { - this.body = []; - this.last_id = null; - this.runtype = ' '; - this.runlen = 0; - this.tail = ''; - this.prefixlen = 0; - this.prefix = ''; - } - - _flushRun () { - if (!this.last_id) return; - this.body.push('@'+this.last_id.toString()); - if (this.tail) { - this.body.push(this.runtype + this.tail); + +class UniRun extends IdRun { + constructor (id, count) { + super(id); + this.count = count ? Base64x64.base2int(count) : 1; + } + toString () { + return this.id + Ids.UNI_RUN + Base64x64.int2base(this.count, 1); + } + at (i) { + return i>=0 && i=this.id.value.length && prefix.length+1>=id.value.length) + // return new Last1Run(this.id).append(id); + // if (prefix.length+2>=this.id.value.length && prefix.length+2>=id.value.length) + // return new Last2Run(this.id).append(id); + return null; } - - appendRun (runstr) { - this._flushRun(); - this.body.push(runstr); + get length () { + return this.count; } +} - _appendToUniRun (id) { - if (id.eq(this.last_id)) { - this.tail = Base64x64.int2base(++this.runlen, 1); + +class Last1Run extends IdRun { + constructor (id, prefix, tail) { + super(id); + this.tail = tail || ''; + this.prefix = null; + } + toString () { + return this.id.value + '-' + this.id.origin + Ids.LAST1_RUN + this.tail; + } + at (i) { + if (i<0 || i>this.tail.length) + return undefined; + if (i===0) + return this.id; + const val1 = this.id.value; + let value = val1.substr(0, val1.length-1) + this.tail.charAt(i-1); + return new Id(value, this.id.origin); + } + append (new_id) { + const id = Id.as(new_id); + if (id.origin!==this.id.origin) + return null; + const common = Base64x64.commonPrefix(this.id.value, id.value); + const tivl = this.id.value.length, ivl = id.value.length, cl = common.length; + if (ivl===tivl && cl===tivl-1) { + this.tail += id.value.substr(plen, 1) || '0'; + return this; + } else if ( (cl===tivl-1 && ivl===tivl+1) || + (cl===tivl-2 && ivl===tivl) ) { + if (this.length>10) return null; + const last2 = new Last2Run(this.id); + for(let i=1; iprefix.length+1) return false; + return true; + } + get length () { + return 1 + this.tail.length; + } +} - _appendToLast2Run (id) { - const val = id.value; - if (val.substr(0, this.prefixlen)!==this.prefix || - val.length>this.prefixlen+2) { - this._flushRun(); - return this.append(id); - } - let two = val.substr(this.prefixlen, this.prefixlen+2); - while (two.length<2) two += '0'; - this.tail += two; - this.runlen++; // vvv - } - - _appendToEmptyRun (id) { - // try start a run - const iv = id.value; - const liv = this.last_id.value; - if (iv===liv) { - this.runtype = Ids.UNI_RUN; - this.runlen = 1; - return this._appendToUniRun(id); + +class Last2Run extends IdRun { + constructor (id, prefix, tail) { + super(id); + this.prefix = prefix || null; // FIXME either-or + this.tail = tail || ''; + } + static fromString (str) { + return Last2Run.fromMatch(m); + } + static fromMatch (m) { + + } + /** @returns {IdRun} */ + append (new_id) { + const id = Id.as(new_id); + if (id.origin!==this.id.origin) + return null; + if (!this.prefix) + this.prefix = Base64x64.commonPrefix(this.id.value, id.value); + const plen = this.prefix.length; + if (this.id.value.length <= plen+2 && id.value.length <= plen+2) { + let last2 = id.value.substr(this.prefix.length); + while (last2.length<2) last2 += '0'; + this.tail += last2; + return this; + } else { + return null; } - const prefix = Base64x64.commonPrefix(iv, liv); - if (iv.length>1 && liv.length>1 && - iv.length<=prefix.length+2 && liv.length<=prefix.length+2) { - this.runtype = Ids.LAST2_RUN; - this.prefixlen = prefix.length; - this.runlen = 1; - this.prefix = prefix; - return this._appendToLast2Run(id); + } + at (i) { + if (i===0) return this.id; + const last2 = this.tail.substr(i*2-2, 2); + return new Id(this.prefix+last2, this.id.origin); + } + toString() { + return this.id + Ids.LAST2_RUN + this.tail; + } + mayHave (id_to_seek) { + const id = Id.as(id_to_seek); + if (this.id.origin!==id.origin) return false; + const prefix = Base64x64.commonPrefix(this.id.value, id.value); + if (prefix.lengthprefix.length+2) return false; + return true; + } + get length () { + return 1 + (this.tail.length>>1); + } +} + + +class Builder { + + constructor () { + this.runs = []; // FIXME toString em + this.open_run = null; + this.str = null; + this.quant = '@'; + } + + appendRun (run_or_str) { + const run = IdRun.as(run_or_str); + if (this.open_id) { + this.runs.push(new IdRun(this.open_id)); + this.open_id = null; } - // if nothing worked - this._flushRun(); - this.last_id = id; - } - - append (id) { - id = Id.as(id); - if (!this.last_id) { - this.last_id = id; - this.runlen = 1; - } else if (id.origin!==this.last_id.origin) { - this._flushRun(); - this.last_id = id; - this.runlen = 1; - } else if (this.runtype===Ids.LAST2_RUN) { - this._appendToLast2Run(id); - } else if (this.runtype===Ids.UNI_RUN) { - this._appendToUniRun(id); + this.runs.push(run.toString()); + this.str = null; + } + + append (new_id) { + this.str = null; + const id = Id.as(new_id); + if (this.open_run) { + const next = this.open_run.append(id); + if (next===null) { + this.runs.push(this.open_run.toString()); + this.open_run = new IdRun(id); + } else { + this.open_run = next; + } } else { - this._appendToEmptyRun(id); + this.open_run = new IdRun(id); } } toString () { - this._flushRun(); - return this.body.join(''); + if (this.str) + return this.str; + this.str = this.runs.length ? this.quant + this.runs.join(this.quant) : ''; + if (this.open_run) + this.str += this.quant + this.open_run.toString(); + return this.str; } } @@ -229,102 +366,53 @@ class Builder { class Iterator { /** @param {Ids} ids */ constructor (ids) { - this.ids = ids._body; - this._m = null; - this._offset = 0; + this.body = ids._body; + this.body_offset = 0; + this.offset = 0; + this.run_offset = 0; + this.open_run = null; this.nextRun(); } get id () { - return this._id; - } - _recover_id () { - switch (this._run_type) { - case Ids.UNI_RUN: - return this._id; - case Ids.LAST2_RUN: - const two = this._run_body.substr((this._run_offset-1)<<1, 2); - return this._id = new Id(this._prefix+two, this._origin); - default: - } + return this.open_run ? this.open_run.at(this.run_offset) : undefined; } next () { const ret = { - value: this._id, + value: this.id, done: this.end }; this.nextId(); return ret; } nextId () { - const ret = this._id; - if (this._run_offset> 1; - break; - case Ids.UNI_RUN: - this._prefix = m[1]; - this._run_length = Base64x64.base2int(this._run_body)-1; - break; - case undefined: - this._run_length = 0; - break; - default: - throw new Error('not implemented yet'); - } - } - get runEndOffset () { - return this._offset - this._run_offset + this._run_length; + Ids.reRun.lastIndex = this.body_offset; + const m = Ids.reRun.exec(this.body); + if (this.open_run) + this.offset += this.open_run.length - this.run_offset; + this.open_run = m ? IdRun.fromMatch(m) : null; + this.run_offset = 0; + this.body_offset = m ? m.index + m[0].length : -1; } - get run () { - return this._run; - } - get runOffset () { - return this._run_offset; - } - get offset () { - return this._offset; + get run_end_offset () { + return this.offset - this.run_offset + this.open_run.length; } runMayHave (id) { - id = Id.as(id); - if (this._id===undefined) return false; - if (this._id.origin!==id.origin) return false; - if (this._run_type===Ids.UNI_RUN) - return this._id.value === id.value; - if (this._prefix!==id.value.substr(0,this._prefix.length)) - return false; - if (this._run_type===Ids.LAST2_RUN && this._prefix.length+2 { + b.append(id); + console.log(id+'\t'+b.toString()); + }); + + const ids = Ids.fromIdArray(id_array); + const id_array2 = ids.toArray(); + + tap.equal(id_array2.length, id_array.length); + tap.equal(id_array2.toString(), id_array.toString()); tap.end(); From 21f13ac9d0e1b9d8d589d9554d2710f128165dbf Mon Sep 17 00:00:00 2001 From: Victor Grishchenko Date: Thu, 29 Dec 2016 15:45:38 +0500 Subject: [PATCH 46/51] padding for last2 ids coding --- swarm-protocol/src/Id.js | 6 ++++++ swarm-protocol/src/Ids.js | 6 ++++-- swarm-protocol/test/07_ids.js | 4 +++- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/swarm-protocol/src/Id.js b/swarm-protocol/src/Id.js index 2fbc59e..3faf999 100644 --- a/swarm-protocol/src/Id.js +++ b/swarm-protocol/src/Id.js @@ -121,6 +121,12 @@ class Id { return this._string; } + toPaddedString (length) { + let v = this._value; + while (v.length Date: Thu, 29 Dec 2016 15:54:20 +0500 Subject: [PATCH 47/51] fix a last2 encoding bug --- swarm-protocol/src/Ids.js | 3 ++- swarm-protocol/test/07_ids.js | 8 ++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/swarm-protocol/src/Ids.js b/swarm-protocol/src/Ids.js index 045daba..0c89365 100644 --- a/swarm-protocol/src/Ids.js +++ b/swarm-protocol/src/Ids.js @@ -286,8 +286,9 @@ class Last2Run extends IdRun { return null; if (!this.prefix) this.prefix = Base64x64.commonPrefix(this.id.value, id.value); + const common = Base64x64.commonPrefix(this.prefix, id.value); const plen = this.prefix.length; - if (this.id.value.length <= plen+2 && id.value.length <= plen+2) { + if (common.length==plen && id.value.length <= plen+2) { let last2 = id.value.substr(this.prefix.length); while (last2.length<2) last2 += '0'; this.tail += last2; diff --git a/swarm-protocol/test/07_ids.js b/swarm-protocol/test/07_ids.js index a0137ec..8a252c1 100644 --- a/swarm-protocol/test/07_ids.js +++ b/swarm-protocol/test/07_ids.js @@ -98,7 +98,7 @@ tap ('protocol.07.C splice', function(tap) { tap.equal(spliced.at(k)+'', arr[k]+'', 'at('+arr[k]+')'); for(let j=0; j<8; j++) { - tap.equal(spliced.find(arr[j]), j, 'find()'); + tap.equal(spliced.find(arr[j]), j, 'find('+arr[j]+')'); } tap.equal(spliced.find(Id.ZERO), arr.indexOf(Id.ZERO)); @@ -117,7 +117,11 @@ tap('protocol.08.D shifts', function (tap) { // :) 'stamp3-author', 'stamp34-author', 'last2-one', - 'last2bb-one' + 'last2bb-one', + 'last2-one', + 'last-one', + 'last-one', + 'last-one' ].map(Id.as); const b = new Ids.Builder(); From fd08c5233674d13ceb5412f034c3da8dce245ca8 Mon Sep 17 00:00:00 2001 From: Victor Grishchenko Date: Thu, 29 Dec 2016 23:50:27 +0500 Subject: [PATCH 48/51] maturing Ops --- swarm-protocol/src/Ids.js | 27 +++-- swarm-protocol/src/Op.js | 65 ++++++++++-- swarm-protocol/src/Ops.js | 195 ++++++++++++++++++++++++++++++++++ swarm-protocol/src/Spec.js | 1 + swarm-protocol/test/08_ops.js | 135 +++++++++++++++++++++++ 5 files changed, 403 insertions(+), 20 deletions(-) create mode 100644 swarm-protocol/test/08_ops.js diff --git a/swarm-protocol/src/Ids.js b/swarm-protocol/src/Ids.js index 0c89365..1424e62 100644 --- a/swarm-protocol/src/Ids.js +++ b/swarm-protocol/src/Ids.js @@ -104,8 +104,8 @@ class Ids { return this.iterator(); } - static fromIdArray (id_array) { - const b = new Builder(); + static fromIdArray (id_array, quant) { + const b = new Builder(quant); id_array.forEach( id => b.append(id) ); return new Ids(b.toString()); } @@ -124,6 +124,9 @@ Ids.LAST1_RUN = "'"; Ids.LAST2_RUN = '"'; Ids.INC_RUN = '`'; + + + /** A run of a single id. */ class IdRun { constructor (id) { @@ -221,7 +224,7 @@ class Last1Run extends IdRun { this.prefix = null; } toString () { - return this.id.value + '-' + this.id.origin + Ids.LAST1_RUN + this.tail; + return this.id.toString() + Ids.LAST1_RUN + this.tail; } at (i) { if (i<0 || i>this.tail.length) @@ -322,18 +325,18 @@ class Last2Run extends IdRun { class Builder { - constructor () { - this.runs = []; // FIXME toString em + constructor (quant) { + this.runs = []; this.open_run = null; this.str = null; - this.quant = '@'; + this.quant = quant || '@'; } appendRun (run_or_str) { const run = IdRun.as(run_or_str); - if (this.open_id) { - this.runs.push(new IdRun(this.open_id)); - this.open_id = null; + if (this.open_run) { + this.runs.push(this.open_run.toString()); // TODO merge + this.open_run = null; } this.runs.push(run.toString()); this.str = null; @@ -417,6 +420,12 @@ class Iterator { get end () { return !this.open_run; } + to (new_offset) { + if (new_offset diff + throw new Error('wrong object'); + const reducer = Op.REDUCERS[state.type] || log_reducer; + const value = reducer(state, op); + return new Op(op.Id, op.Type, op.Stamp, Spec.STATE_OP_NAME, value); + } + } + Op.NON_SPECIFIC_NOOP = new Op(Spec.NON_SPECIFIC_NOOP, ""); Op.SERIALIZATION_MODES = { LINE_BASED: 1, @@ -160,4 +184,23 @@ Op.reOp = new RegExp(Op.rsOp, "mg"); Op.ZERO = new Op(new Spec(), null); Op.CLASS_HANDSHAKE = "Swarm"; +Op.DB_TYPE_NAME = 'db'; +Op.DB_TYPE_ID = new Id(Op.DB_TYPE_NAME); + +Op.REDUCERS = Object.create(null); + +function log_reducer (state, op) { + return null; +} + +function lww_reducer (state, op) { + const ops = state.ops; + const i = ops.findLoc(op.Loc); + return ops.splice(i, 1, [op]); +} + +Op.REDUCERS.json = lww_reducer; +Op.REDUCERS.db = lww_reducer; +Op.REDUCERS.log = log_reducer; + module.exports = Op; diff --git a/swarm-protocol/src/Ops.js b/swarm-protocol/src/Ops.js index e69de29..8d20964 100644 --- a/swarm-protocol/src/Ops.js +++ b/swarm-protocol/src/Ops.js @@ -0,0 +1,195 @@ +"use strict"; +const Base64x64 = require('./Base64x64'); +const Id = require('./Id'); +const Spec = require('./Spec'); +const Op = require('./Op'); +const Ids = require('./Ids'); + +/** immutable op array - same object ops only */ +class Ops { + + constructor (id, type, stamps, locs, vals) { + this._id = id; + this._type = type; + this._stamps = Ids.as(stamps); + this._locs = Ids.as(locs); + this._values = vals; + this._string = null; + } + + static fromOp (op) { + const val = op.Value || {s:'',l:'',v:[]}; + return new Ops(op.Id, op.Type, val.s, val.l, val.v); + } + + static fromOpArray (ops) { + const stamps = ops.map( op => op.Stamp ); + const locs = ops.map( op => op.Loc ); + const vals = ops.map( op => op.Value ); + return new Ops( + ops[0].Id, + ops[0].Type, + Ids.fromIdArray(stamps, '@'), + Ids.fromIdArray(locs, ':'), + vals + ); + } + + get stamps () { + return this._stamps; + } + + get locations () { + return this._locs; + } + + get values () { + return this._values; + } + + splice (pos, remove, insert_ops) { + const new_stamps = insert_ops.map( op => op.Stamp ); + const new_locs = insert_ops.map( op => op.Location ); + const new_vals = insert_ops.map( op => op.Value ); + const stamps = this._stamps.splice(pos, remove, new_stamps); + const locs = this._locs.splice(pos, remove, new_locs); + const values = this._values.slice(0, pos).concat( + new_vals, this._values.slice(pos+remove) ); + return new Ops(this._id, this._type, stamps, locs, values); + } + + iterator (context) { + return new OpsIterator(this, context); + } + + [Symbol.iterator]() { + return this.iterator(); + } + + /** @return {Ops} */ + filter (fn) { + + } + + get length () { + return this._stamps.length; + } + + forEach (fn) { + for(let op of this) + fn(op); + } + + /** get value by location */ + get (id) { + const i = this.findLoc(id); + return i===-1 ? undefined : this._values[i]; + } + + findLoc (loc_id) { + return this._locs.find(loc_id); + } + + findStamp () { + + } + + at (i) { + return new Op( + this._id, + this._type, + this._stamps.at(i), + this._locs.at(i), + this._values[i] + ); + } + + toArray (context) { + return Array.from(this.iterator(context)); + } + + toOp (spec) { + return new Op( spec.Id, spec.Type, spec.Stamp, spec.Location, this.toJSON() ); + } + + toJSON () { + return { + s: this._stamps.toString(), + l: this._locs.toString(), + v: this._values + }; + } + + toString () { + if (this._string===null) { + this._string = JSON.stringify(this.toJSON()); + } + return this._string; + } + +} + +class OpsIterator { + + constructor (ops) { + this._ops = ops; + this._op = null; + this.stamps = ops._stamps.iterator(); + this.locs = ops._locs.iterator(); + this.vals = ops._values[Symbol.iterator](); + this.nextOp(); + } + + get op () { + return this._op; + } + + get Stamp () { + + } + + get Location () { + + } + + get Value () { + + } + + to (new_offset) { + while (!this.stamps.end && this.stamps.offset Date: Mon, 2 Jan 2017 22:55:09 +0500 Subject: [PATCH 49/51] new syntax vim hili file --- swarm-bat/vim/syntax/batt.vim | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/swarm-bat/vim/syntax/batt.vim b/swarm-bat/vim/syntax/batt.vim index 9f48d53..1f25760 100644 --- a/swarm-bat/vim/syntax/batt.vim +++ b/swarm-bat/vim/syntax/batt.vim @@ -8,10 +8,12 @@ endif syn match batCommentLine /^;.*$/ syn match batInputLine /^[a-zA-Z0-9]*>/ syn match batOutputLine /^[a-zA-Z0-9]* Date: Tue, 3 Jan 2017 17:49:40 +0500 Subject: [PATCH 50/51] {id:stamp} VersionMap class --- swarm-cli/src/FileCache.js | 0 swarm-cli/src/LevelNodeStore.js | 9 ++++++ swarm-protocol/index.js | 4 ++- swarm-protocol/src/VersionMap.js | 44 ++++++++++++++++++++++++++++ swarm-protocol/test/0B_VersionMap.js | 24 +++++++++++++++ 5 files changed, 80 insertions(+), 1 deletion(-) delete mode 100644 swarm-cli/src/FileCache.js create mode 100644 swarm-cli/src/LevelNodeStore.js create mode 100644 swarm-protocol/src/VersionMap.js create mode 100644 swarm-protocol/test/0B_VersionMap.js diff --git a/swarm-cli/src/FileCache.js b/swarm-cli/src/FileCache.js deleted file mode 100644 index e69de29..0000000 diff --git a/swarm-cli/src/LevelNodeStore.js b/swarm-cli/src/LevelNodeStore.js new file mode 100644 index 0000000..07af012 --- /dev/null +++ b/swarm-cli/src/LevelNodeStore.js @@ -0,0 +1,9 @@ + + +// leveldb for the store, that's it +// JSON files come in a separate layer + + +class NodeClient { + +} \ No newline at end of file diff --git a/swarm-protocol/index.js b/swarm-protocol/index.js index 6bc28e6..2d8babe 100644 --- a/swarm-protocol/index.js +++ b/swarm-protocol/index.js @@ -7,10 +7,12 @@ var Swarm = { Clock: require('./src/Clock'), Spec: require('./src/Spec'), Op: require('./src/Op'), + Ops: require('./src/Ops'), VV: require('./src/VV'), Ids: require('./src/Ids'), ReplicaId: require('./src/ReplicaId'), - ReplicaIdScheme: require('./src/ReplicaIdScheme') + ReplicaIdScheme: require('./src/ReplicaIdScheme'), + VersionMap: require('./src/VersionMap') }; module.exports = Swarm; diff --git a/swarm-protocol/src/VersionMap.js b/swarm-protocol/src/VersionMap.js new file mode 100644 index 0000000..da97122 --- /dev/null +++ b/swarm-protocol/src/VersionMap.js @@ -0,0 +1,44 @@ +"use strict"; +const Id = require('./Id'); +const Ids = require('./Ids'); + +/** id-to-stamp map */ +class VersionMap { + + constructor (ids, stamps) { + this._map = Object.create(null); + const i = Ids.as(ids).iterator(); + const s = Ids.as(stamps).iterator(); + while (!i.end && !s.end) { + this._map[i.nextId()] = s.nextId().toString(); + } + } + + set (id, stamp) { + this._map[Id.as(id).toString()] = stamp.toString(); + } + + get (id) { + const got = this._map[Id.as(id).toString()]; + return Id.as(got||'0'); + } + + toString () { + const ids = Object.keys(this._map); + const stamps = ids.map( k => this._map[k] ); + const i = Ids.fromIdArray(ids, '#'); + const s = Ids.fromIdArray(stamps, '@'); + return i.toString() + s.toString(); + // TODO sort by origin/value + } + + static fromString (string) { + const i = string.indexOf('@'); + const ids = string.substr(0, i); + const stamps = string.substr(i); + return new VersionMap(ids, stamps); + } + +} + +module.exports = VersionMap; \ No newline at end of file diff --git a/swarm-protocol/test/0B_VersionMap.js b/swarm-protocol/test/0B_VersionMap.js new file mode 100644 index 0000000..6f67c06 --- /dev/null +++ b/swarm-protocol/test/0B_VersionMap.js @@ -0,0 +1,24 @@ +"use strict"; +const tape = require('tape').test; +const swarm = require('swarm-protocol'); +const Id = swarm.Id; +const VersionMap = swarm.VersionMap; + +tape ('protocol.0B.1 version map ser/deser', function (t) { + + const map = new VersionMap(); + map.set("000", "x"); + map.set("~000", "x"); + map.set(Id.ZERO, Id.NEVER); + map.set(Id.NEVER, Id.ZERO); + + t.equal(map.toString(), "#0#~@~@0"); + + const deser = VersionMap.fromString(map.toString()); + + t.equal(deser.get(Id.ZERO)+'', Id.NEVER+''); + t.equal(deser.get(Id.NEVER)+'', Id.ZERO+''); + + t.end(); + +}); \ No newline at end of file From 6d1b4c285bd3209798625dde91e64f59fd51884b Mon Sep 17 00:00:00 2001 From: Victor Grishchenko Date: Thu, 5 Jan 2017 13:22:47 +0500 Subject: [PATCH 51/51] the json big-object package --- swarm-cli/src/LevelNodeStore.js | 9 --- swarm-cli/src/NodeFileStore.js | 24 ++++++++ swarm-json/Makefile | 1 + swarm-json/package.json | 33 +++++++++++ swarm-json/src/Json.js | 98 ++++++++++++++++++++++++++++++++ swarm-json/test/01_oplog2json.js | 27 +++++++++ swarm-protocol/src/OpStream.js | 20 +++++-- 7 files changed, 197 insertions(+), 15 deletions(-) delete mode 100644 swarm-cli/src/LevelNodeStore.js create mode 100644 swarm-cli/src/NodeFileStore.js create mode 100644 swarm-json/Makefile create mode 100644 swarm-json/package.json create mode 100644 swarm-json/src/Json.js create mode 100644 swarm-json/test/01_oplog2json.js diff --git a/swarm-cli/src/LevelNodeStore.js b/swarm-cli/src/LevelNodeStore.js deleted file mode 100644 index 07af012..0000000 --- a/swarm-cli/src/LevelNodeStore.js +++ /dev/null @@ -1,9 +0,0 @@ - - -// leveldb for the store, that's it -// JSON files come in a separate layer - - -class NodeClient { - -} \ No newline at end of file diff --git a/swarm-cli/src/NodeFileStore.js b/swarm-cli/src/NodeFileStore.js new file mode 100644 index 0000000..97ef98e --- /dev/null +++ b/swarm-cli/src/NodeFileStore.js @@ -0,0 +1,24 @@ +"use strict"; +const protocol = require('swarm-protocol'); + +// leveldb for the store, that's it +// JSON files come in a separate layer + + +class LevelNodeStore extends protocol.Client.Store { + + constructor (db) { + this.db = db; + } + + get (keys, callback) { + this.db.get(); + } + + set (keys_values, callback) { + this.db.batch(); + } + +} + +module.exports = LevelNodeStore; \ No newline at end of file diff --git a/swarm-json/Makefile b/swarm-json/Makefile new file mode 100644 index 0000000..0c937fd --- /dev/null +++ b/swarm-json/Makefile @@ -0,0 +1 @@ +include ../Makefile.package.in diff --git a/swarm-json/package.json b/swarm-json/package.json new file mode 100644 index 0000000..2897f44 --- /dev/null +++ b/swarm-json/package.json @@ -0,0 +1,33 @@ +{ + "name": "swarm-json", + "version": "1.4.0", + "homepage": "http://github.com/gritzko/swarm", + "repository": { + "type": "git", + "url": "https://github.com/gritzko/swarm.git" + }, + "author": { + "email": "victor.grishchenko@gmail.com", + "name": "Victor Grishchenko" + }, + "email": "swarm.js@gmail.com", + "license": "MIT", + "files": [ + "index.js", + "src/*.js", + "test/*.js", + "LICENSE", + "README.md" + ], + "main": "index.js", + "browser": "index.js", + "dependencies": { + }, + "devDependencies": { + "tape": "4.6.2", + "tap-diff": "^0.1.1" + }, + "scripts": { + "test": "make test" + } +} diff --git a/swarm-json/src/Json.js b/swarm-json/src/Json.js new file mode 100644 index 0000000..013123a --- /dev/null +++ b/swarm-json/src/Json.js @@ -0,0 +1,98 @@ +"use strict"; +const swarm = require('swarm-protocol'); +const Id = swarm.Id; +const Spec = swarm.Spec; +const Op = swarm.Op; +const OpStream = swarm.OpStream; +const Ops = swarm.Ops; + + +// TODO immutable +class Json extends OpStream { + + constructor (id, client, options) { + super(options); + this._client = client; + this._id = Id.as(id); + this._pieces = Object.create(null); // { id : js_obj } + this._root = this._request(this._id); + } + + /** request the state for a new piece */ + _request (id) { + const piece = Object.create(null); + this._pieces[this._id] = piece; + this._client.onObject(id, this); + return piece; + } + + /** rebuild a piece, maybe create new pieces recursively */ + _emitted (on) { + // check fields + const piece = this._pieces[on.id]; + const names = Object.create(null); + if (!piece) return; + const ops = Ops.fromOp(on); + for(const op of ops) { + const name = op.loc; + const value = op.Value; + names[name] = 1; + if (value instanceof Spec) { + if (value.id in this._pieces) { + piece[name] = this._pieces[value.id]; + } else { + piece[name] = this._request(value.id); + } + } else { + piece[name] = op.Value; + } + } + for(let field in piece) + if (!names[field]) + delete piece[field]; + } + + _piece2id (piece) { // TODO nicer + for(let id in this._pieces) + if (this._pieces[id]===piece) + return id; + return Id.ZERO; + } + + setPath (path, value) { + // find the id + if (path instanceof String) + path = path.split(/[\.\/]/g); + if (!(path instanceof Array)) + throw new Error("specify a path.to.the.field"); + const p = path.slice(); + let piece = this._root; + while (p.length>1) { + piece = piece[p.shift()]; + if (!piece) + throw new Error('the path leads nowhere'); + } + const field = p.shift(); + // value 2 ref + this.setField(piece, field, value); + } + + setField (piece, field, value) { + const id = this._piece2id(piece); + if (id.isZero()) + throw new Error('unknown JSON piece'); + const op = new Op(id, "json", Id.ZERO, field, value); + this._client.commit(op); + } + + get json () { + return this._root; + } + + save () { + // ???!!!! TODO walk it + } + +} + +module.exports = Json; \ No newline at end of file diff --git a/swarm-json/test/01_oplog2json.js b/swarm-json/test/01_oplog2json.js new file mode 100644 index 0000000..bad5937 --- /dev/null +++ b/swarm-json/test/01_oplog2json.js @@ -0,0 +1,27 @@ +"use strict"; +const tape = require('tape').test; +const swarm = require('swarm-protocol'); +const Id = swarm.Id; +const Op = swarm.Op; +const OpStream = swarm.OpStream; +const Json = require('../src/Json'); + +tape ('json.01.A simple build', function (t) { + + const ops = Op.parseFrame([ + '#id-author.json@time1-origin:~on={"s":"@time-or","l":":field","v":[1]}' + ].join('\n')+'\n'); + + const client = new OpStream.ZeroOpStream(); + client.onObject = function (id, os) { this.on(os); }; // TODO upstream + const json = new Json("id-author", client, {}); + + ops.forEach( o => json._emitted(o) ); + + const j = json.json; + + t.deepEqual(j, {field:1}); + + t.end(); + +}); diff --git a/swarm-protocol/src/OpStream.js b/swarm-protocol/src/OpStream.js index de3347d..2cbc623 100644 --- a/swarm-protocol/src/OpStream.js +++ b/swarm-protocol/src/OpStream.js @@ -34,7 +34,7 @@ class OpStream { on (opstream) { if (opstream.constructor===Function) opstream = new CallbackOpStream(opstream); - if (!opstream._emitted || opstream._emitted.constructor!==Function) + if (! (opstream instanceof OpStream) ) throw new Error('opstreams only'); this._lstn.push(opstream); return opstream; @@ -140,13 +140,21 @@ class OpStream { this.on( new FilterOpStream( o => o==null, new CallbackOpStream(callback) ) ); } + /** Normalize opstream/callback to an opstream. */ + static as (stream) { + if (stream && stream.constructor===Function) + return new CallbackOpStream(stream); + if (!stream || !stream._emitted || !stream._committed) + throw new Error('not an opstream'); + return stream; + } + static connect (url, options) { if (url.constructor!==URL) url = new URL(url.toString()); - const top_proto = url.scheme[0]; - const fn = OpStream._URL_HANDLERS[top_proto]; + const fn = OpStream._URL_HANDLERS[url.protocol]; if (!fn) - throw new Error('unknown protocol: '+top_proto); + throw new Error('unknown protocol: '+url.protocol); return new fn(url, options||Object.create(null)); } @@ -254,6 +262,6 @@ class FilterOpStream extends OpStream { } -FilterOpStream.rsTok = '([/#!\\.])(' + swarm.Stamp.rsTok + ')'; -FilterOpStream.reTok = new RegExp(FilterOpStream.rsTok, 'g'); +// FilterOpStream.rsTok = '([/#!\\.])(' + swarm.Id.rsTok + ')'; +// FilterOpStream.reTok = new RegExp(FilterOpStream.rsTok, 'g'); OpStream.Filter = FilterOpStream;