From 7991a5df78a588b1a2f02822f1b048d5c574eb85 Mon Sep 17 00:00:00 2001 From: "Franz Heinzmann (Frando)" Date: Tue, 22 Oct 2019 13:45:08 +0200 Subject: [PATCH 01/85] Move index.js to kappa-old.js --- index.js | 130 +-------------------------------------------------- kappa-old.js | 129 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 131 insertions(+), 128 deletions(-) create mode 100644 kappa-old.js diff --git a/index.js b/index.js index 4c95c4f..5b17589 100644 --- a/index.js +++ b/index.js @@ -1,129 +1,3 @@ -var inherits = require('inherits') -var EventEmitter = require('events').EventEmitter -var hypercore = require('hypercore') -var multifeed = require('multifeed') -var indexer = require('multifeed-index') +const kappa = require('./kappa-old.js') -module.exports = Kappa - -function Kappa (storage, opts) { - if (!(this instanceof Kappa)) return new Kappa(storage, opts) - if (!opts) opts = {} - - this._logs = opts.multifeed || multifeed(hypercore, storage, opts) - this._indexes = {} - - this.api = {} -} - -inherits(Kappa, EventEmitter) - -Kappa.prototype.use = function (name, version, view) { - var self = this - if (typeof version !== 'number') { - view = version - version = undefined - } - var idx = indexer(Object.assign({}, view, { - log: this._logs, - version: version, - maxBatch: view.maxBatch || 10, - batch: view.map - })) - idx.on('error', function (err) { - self.emit('error', err) - }) - if (view.indexed) idx.on('indexed', view.indexed) - this._indexes[name] = idx - this.api[name] = {} - this.api[name].ready = idx.ready.bind(idx) - for (var key in view.api) { - if (typeof view.api[key] === 'function') this.api[name][key] = view.api[key].bind(idx, this) - else this.api[name][key] = view.api[key] - } -} - -Kappa.prototype.feeds = function () { - return this._logs.feeds() -} - -Kappa.prototype.ready = function (viewNames, cb) { - if (typeof viewNames === 'function') { - cb = viewNames - viewNames = [] - } - - if (typeof viewNames === 'string') viewNames = [viewNames] - if (viewNames.length === 0) { - viewNames = Object.keys(this._indexes) - } - - var pending = viewNames.length + 1 - var self = this - this._logs.ready(function () { - for (var i = 0; i < viewNames.length; i++) { - self._indexes[viewNames[i]].ready(done) - } - done() - }) - - function done () { - if (!--pending) cb() - } -} - -Kappa.prototype.pause = function (viewNames, cb) { - if (typeof viewNames === 'function') { - cb = viewNames - viewNames = [] - } - cb = cb || noop - - if (!viewNames) viewNames = [] - if (typeof viewNames === 'string') viewNames = [viewNames] - if (viewNames.length === 0) { - viewNames = Object.keys(this._indexes) - } - - var pending = viewNames.length + 1 - var self = this - this._logs.ready(function () { - for (var i = 0; i < viewNames.length; i++) { - self._indexes[viewNames[i]].pause(done) - } - done() - }) - - function done () { - if (!--pending) cb() - } -} - -Kappa.prototype.resume = function (viewNames) { - if (!viewNames) viewNames = [] - if (typeof viewNames === 'string') viewNames = [viewNames] - if (viewNames.length === 0) { - viewNames = Object.keys(this._indexes) - } - - var self = this - this._logs.ready(function () { - for (var i = 0; i < viewNames.length; i++) { - self._indexes[viewNames[i]].resume() - } - }) -} - -Kappa.prototype.writer = function (name, cb) { - this._logs.writer(name, cb) -} - -Kappa.prototype.feed = function (key) { - return this._logs.feed(key) -} - -Kappa.prototype.replicate = function (opts) { - return this._logs.replicate(opts) -} - -function noop () {} +module.exports = kappa diff --git a/kappa-old.js b/kappa-old.js new file mode 100644 index 0000000..4c95c4f --- /dev/null +++ b/kappa-old.js @@ -0,0 +1,129 @@ +var inherits = require('inherits') +var EventEmitter = require('events').EventEmitter +var hypercore = require('hypercore') +var multifeed = require('multifeed') +var indexer = require('multifeed-index') + +module.exports = Kappa + +function Kappa (storage, opts) { + if (!(this instanceof Kappa)) return new Kappa(storage, opts) + if (!opts) opts = {} + + this._logs = opts.multifeed || multifeed(hypercore, storage, opts) + this._indexes = {} + + this.api = {} +} + +inherits(Kappa, EventEmitter) + +Kappa.prototype.use = function (name, version, view) { + var self = this + if (typeof version !== 'number') { + view = version + version = undefined + } + var idx = indexer(Object.assign({}, view, { + log: this._logs, + version: version, + maxBatch: view.maxBatch || 10, + batch: view.map + })) + idx.on('error', function (err) { + self.emit('error', err) + }) + if (view.indexed) idx.on('indexed', view.indexed) + this._indexes[name] = idx + this.api[name] = {} + this.api[name].ready = idx.ready.bind(idx) + for (var key in view.api) { + if (typeof view.api[key] === 'function') this.api[name][key] = view.api[key].bind(idx, this) + else this.api[name][key] = view.api[key] + } +} + +Kappa.prototype.feeds = function () { + return this._logs.feeds() +} + +Kappa.prototype.ready = function (viewNames, cb) { + if (typeof viewNames === 'function') { + cb = viewNames + viewNames = [] + } + + if (typeof viewNames === 'string') viewNames = [viewNames] + if (viewNames.length === 0) { + viewNames = Object.keys(this._indexes) + } + + var pending = viewNames.length + 1 + var self = this + this._logs.ready(function () { + for (var i = 0; i < viewNames.length; i++) { + self._indexes[viewNames[i]].ready(done) + } + done() + }) + + function done () { + if (!--pending) cb() + } +} + +Kappa.prototype.pause = function (viewNames, cb) { + if (typeof viewNames === 'function') { + cb = viewNames + viewNames = [] + } + cb = cb || noop + + if (!viewNames) viewNames = [] + if (typeof viewNames === 'string') viewNames = [viewNames] + if (viewNames.length === 0) { + viewNames = Object.keys(this._indexes) + } + + var pending = viewNames.length + 1 + var self = this + this._logs.ready(function () { + for (var i = 0; i < viewNames.length; i++) { + self._indexes[viewNames[i]].pause(done) + } + done() + }) + + function done () { + if (!--pending) cb() + } +} + +Kappa.prototype.resume = function (viewNames) { + if (!viewNames) viewNames = [] + if (typeof viewNames === 'string') viewNames = [viewNames] + if (viewNames.length === 0) { + viewNames = Object.keys(this._indexes) + } + + var self = this + this._logs.ready(function () { + for (var i = 0; i < viewNames.length; i++) { + self._indexes[viewNames[i]].resume() + } + }) +} + +Kappa.prototype.writer = function (name, cb) { + this._logs.writer(name, cb) +} + +Kappa.prototype.feed = function (key) { + return this._logs.feed(key) +} + +Kappa.prototype.replicate = function (opts) { + return this._logs.replicate(opts) +} + +function noop () {} From 344266649aa40e06f366e8c11b0ef356f73a3cec Mon Sep 17 00:00:00 2001 From: "Franz Heinzmann (Frando)" Date: Tue, 22 Oct 2019 13:45:43 +0200 Subject: [PATCH 02/85] Add new Kappa! This is a dependency-free version of the kappa core idea. It connects sources to views and tracks state. Next commit will add a kappaClassic wrapper that support multifeed by default. --- kappa.js | 361 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 361 insertions(+) create mode 100644 kappa.js diff --git a/kappa.js b/kappa.js new file mode 100644 index 0000000..83d82da --- /dev/null +++ b/kappa.js @@ -0,0 +1,361 @@ +const thunky = require('thunky') +const { EventEmitter } = require('events') + +const Status = { + Ready: 0, + Running: 1, + Paused: 2 +} + +module.exports = class Kappa extends EventEmitter { + /** + * Create a kappa core. + * @constructor + */ + constructor (opts = {}) { + super() + this.opts = opts + this.views = {} + this.sources = {} + this.api = {} + this.flows = [] + this._flowsByName = {} + this._states = {} + this._status = Status.Ready + this.ready = thunky(this._ready.bind(this)) + } + + /** + * Register a view in the kappa. + * + * @param {string} name a unique name for this view + * @param {object} view handlers object + * @param {function(msgs, next)} view.map process messages + * @param {function(msgs, next)} [view.filter] filter messages before map + * @param {function(msgs, next)} [view.clearIndex] clear index + */ + use (name, view) { + const self = this + view.name = name + view.api = view.api || {} + + this.views[name] = view + this.api[name] = { + ready (cb) { + self.onViewIndexed(name, cb) + } + } + + for (let [key, value] of Object.entries(view.api)) { + if (typeof value === 'function') value = value.bind(this.api[name], this) + this.api[name][key] = value + } + + if (this.opts.autoconnect || view.autoconnect) { + Object.keys(this.sources).forEach(sourceName => this.connect(sourceName, name)) + this.on('source', sourceName => this.connect(sourceName, name)) + } + + this.emit('view', view.name, view) + } + + /** + * Register a source in the kappa. + * + * @param {string} name a unique name for this source + * @param {function} createSource source constructor + * @param {opts} opts + */ + source (name, createSource, opts = {}) { + if (this.sources[name]) return + opts.name = name + this.sources[name] = { createSource, opts, name } + + if (this.opts.autoconnect || opts.autoconnect) { + Object.keys(this.views).forEach(viewName => this.connect(name, viewName)) + this.on('view', viewName => this.connect(name, viewName)) + } + + this.emit('source', name) + } + + /** + * Connect a source to a view. Creates a new flow. + * + * @param {string} sourceName + * @param {string} viewName + * @return {object} flow + */ + connect (sourceName, viewName) { + if (!this.views[viewName]) throw new Error('Unknown view: ' + viewName) + if (!this.sources[sourceName]) throw new Error('Unknown source: ' + sourceName) + + const flowName = sourceName + '~' + viewName + if (this._flowsByName[flowName]) return this._flowsByName[flowName] + + const view = this.views[viewName] + const { createSource, opts } = this.sources[sourceName] + + const flow = new Flow(this, view, createSource, opts) + + this.flows.push(flow) + this._flowsByName[flow.name] = flow + + if (this.opts.autostart) flow.open() + + this.emit('connect', sourceName, viewName) + + return flow + } + + connectAll () { + for (const source of Object.values(this.sources)) { + if (source.parent) continue + for (const view of Object.values(this.views)) { + this.connect(source.name, view.name) + } + } + } + + onViewIndexed (name, cb) { + const flows = this._flowsByView(name).filter(f => !f.parent) + let pending = flows.length + 1 + flows.forEach(flow => flow.ready(done)) + done() + function done () { + if (--pending === 0) cb() + } + } + + _ready (cb) { + if (this.opts.autoconnect) this.connectAll() + + let pending = this.flows.length + 1 + this.flows.forEach(flow => flow.open(finish)) + finish() + + function finish () { + if (--pending === 0) process.nextTick(cb) + } + } + + pause (cb) { + this.status = Status.Paused + this.flows.forEach(flow => flow.pause()) + } + + resume (cb) { + if (this.status === Status.Paused) { + this.flows.forEach(flow => flow.resume()) + this.status = Status.Ready + } + } + + clear (viewName, cb) { + const view = this.views[viewName] + if (!view) throw new Error('Unknown view: ' + viewName) + const flows = this._flowsByView(viewName) + + flows.forEach(flow => flow.pause()) + if (view.clearIndex) view.clearIndex(restartFlows) + else restartFlows() + + let pending + function restartFlows () { + pending = flows.length + 1 + flows.forEach(flow => flow.restart(finish)) + finish() + } + function finish () { + if (--pending === 0) cb() + } + } + + _flowsByView (name) { + return this.flows.filter(flow => flow.view.name === name) + } + + _flowsBySource (name) { + return this.flows.filter(flow => flow.source.name === name) + } + + _fetchState (flow, cb) { + // if (flow.view.fetchState) return flow.view.fetchState(flow, cb) + cb(null, this._states[flow.name]) + } + + _storeState (flow, state, cb) { + // if (flow.view.storeState) return flow.view.storeState(flow, state, cb) + this._states[flow.name] = state + cb() + } +} + +class Flow extends EventEmitter { + constructor (kappa, view, createSource, opts) { + super() + this.kappa = kappa + this.view = view + this.opts = opts + + this.source = createSource({ + onupdate: this._onupdate.bind(this), + onerror: this._onerror.bind(this), + onsource: this._onsource.bind(this) + }, opts) + if (!this.source.name) this.source.name = opts.name + + this.name = this.source.name + '~' + this.view.name + this.parent = opts.parent + + this.subflows = [] + this.status = Status.Ready + this._opened = false + this.open = thunky(this._open.bind(this)) + } + + _open (cb) { + if (this._opened) return cb() + const self = this + + let pending = this.subflows.length + 1 + + if (this.view.open) ++pending && this.view.open(finish) + if (this.source.open) ++pending && this.source.open(finish) + this.subflows.forEach(flow => flow.open(finish)) + + finish() + + function finish () { + if (!(--pending === 0)) return + // self.status = Status.Ready + self._opened = true + self._run() + if (cb) cb() + } + } + + ready (cb = noop) { + const self = this + + if (!this.opened) { + return this.open(() => this.ready(cb)) + } + + let pending = this.subflows.length + 1 + this.subflows.forEach(flow => flow.ready(finish)) + finish() + function finish () { + if (--pending !== 0) return + if (self.status === Status.Ready) process.nextTick(cb) + else self.once('ready', cb) + } + } + + pause () { + this.status = Status.Paused + } + + resume () { + if (this.status !== Status.Paused) return + this.status = Status.Ready + if (!this._opened) this.open() + else this._run() + } + + restart (cb) { + this.pause() + process.nextTick(() => { + this.kappa._storeState(this, null, () => { + this.resume() + if (cb) cb() + }) + }) + } + + _onupdate () { + if (!this._opened) return + this.incomingUpdate = true + process.nextTick(this._run.bind(this)) + } + + _onbatch (msgs, cb) { + const { source, view } = this + + const steps = [ + source.transform, + view.filter, + view.transform + ].filter(fn => fn) + + runAll(msgs, steps, msgs => { + if (!msgs.length) cb(null, msgs) + else view.map(msgs, () => cb(null, msgs)) + }) + } + + _run () { + const self = this + if (!this._opened) throw new Error('Flow is not opened.') + if (this.status === Status.Running) return + if (this.status === Status.Paused) return + + this.status = Status.Running + + this.kappa._fetchState(this, (err, state) => { + if (err) return this._onerror(err) + this.source.start(state, onbatch) + }) + + function onbatch (nextState, msgs, workMore) { + msgs = msgs || [] + if (self.status === Status.Paused) return close() + self._onbatch(msgs, (err, msgs) => { + if (err) return self._onerror(err) + self.kappa._storeState(self, nextState, err => { + if (err) return self._onerror(err) + close(msgs, workMore) + }) + }) + } + + function close (msgs, workMore) { + self.status = Status.Ready + if (msgs.length && self.view.indexed) { + self.view.indexed(msgs) + } + if (self.incomingUpdate || workMore) { + self.incomingUpdate = false + process.nextTick(self._run.bind(self)) + } else { + self.emit('ready') + } + } + } + + _onerror (err) { + this.kappa.emit(err, this) + } + + _onsource (name, createSource, opts) { + const fullname = this.source.name + ':' + name + opts.parent = this.name + this.kappa.source(fullname, createSource, opts) + const flow = this.kappa.connect(fullname, this.view.name) + this.subflows.push(flow) + flow.open() + } +} + +function runAll (state, fns, final) { + next(state) + function next (state) { + const fn = fns.shift() + if (!fn) return final(state) + fn(state, nextState => { + process.nextTick(next, nextState) + }) + } +} + +function noop () {} From 9472c736c4ab298daca7cb437a263834677d850d Mon Sep 17 00:00:00 2001 From: "Franz Heinzmann (Frando)" Date: Tue, 22 Oct 2019 13:49:31 +0200 Subject: [PATCH 03/85] Add kappaClassic wrapper and source handlers --- index.js | 24 ++++++++++++++++++++++-- sources/hypercore.js | 32 ++++++++++++++++++++++++++++++++ sources/multifeed.js | 31 +++++++++++++++++++++++++++++++ 3 files changed, 85 insertions(+), 2 deletions(-) create mode 100644 sources/hypercore.js create mode 100644 sources/multifeed.js diff --git a/index.js b/index.js index 5b17589..85450af 100644 --- a/index.js +++ b/index.js @@ -1,3 +1,23 @@ -const kappa = require('./kappa-old.js') +const Kappa = require('./kappa') -module.exports = kappa +// module.exports = (...args) => new Kappa(...args) +module.exports = kappaClassic +module.exports.Kappa = Kappa + +function kappaClassic (storage, opts = {}) { + const multifeed = require('multifeed') + const hypercore = require('hypercore') + const multifeedSource = require('./sources/multifeed') + + const feeds = opts.multifeed || multifeed(hypercore, storage, opts) + + const kappa = new Kappa({ autoconnect: true, autostart: true }) + + kappa.source('multifeed', multifeedSource, { feeds }) + + kappa.writer = feeds.writer.bind(feeds) + kappa.feed = feeds.feed.bind(feeds) + kappa.replicate = feeds.replicate.bind(feeds) + + return kappa +} diff --git a/sources/hypercore.js b/sources/hypercore.js new file mode 100644 index 0000000..be7856e --- /dev/null +++ b/sources/hypercore.js @@ -0,0 +1,32 @@ +module.exports = function hypercoreSource (handlers, opts) { + const maxBatch = opts.maxBatch || 50 + const feed = opts.feed + + return { open, start } + + function open (next) { + feed.ready(function () { + feed.on('append', handlers.onupdate) + feed.on('download', handlers.onupdate) + next() + }) + } + + function start (state, next) { + const at = state || 0 + const to = Math.min(feed.length, at + maxBatch) + if (!(to > at)) return next(at) + if (!feed.has(at, to)) { + return next(at) + } + feed.getBatch(at, to, { wait: false }, (err, res) => { + if (err) return handlers.onerror(err) + res = res.map((node, i) => ({ + key: feed.key.toString('hex'), + seq: at + i, + value: node + })) + next(to, res, to < feed.length) + }) + } +} diff --git a/sources/multifeed.js b/sources/multifeed.js new file mode 100644 index 0000000..22cc78f --- /dev/null +++ b/sources/multifeed.js @@ -0,0 +1,31 @@ +const hypercoreSource = require('./hypercore') + +module.exports = function multifeedSource (handlers, opts) { + const feeds = opts.feeds + + return { open, start } + + function start (state, next) { + next(state) + } + + function open (next) { + feeds.ready(() => { + let pending = feeds.feeds().length + 1 + feeds.feeds().forEach(feed => _onfeed(feed, done)) + feeds.on('feed', feed => _onfeed(feed)) + done() + function done () { + if (!--pending) next() + } + }) + } + + function _onfeed (feed, cb) { + feed.ready(() => { + const name = feed.key.toString('hex') + handlers.onsource(name, hypercoreSource, { feed }) + if (cb) cb() + }) + } +} From 8160fb54133ee8942820d1746ad4285e787abe09 Mon Sep 17 00:00:00 2001 From: "Franz Heinzmann (Frando)" Date: Tue, 22 Oct 2019 13:50:06 +0200 Subject: [PATCH 04/85] Add thunky dependency --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 82069e1..561fb6e 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,8 @@ "hypercore": "^7.4.0", "inherits": "^2.0.4", "multifeed": "^4.0.0", - "multifeed-index": "^3.3.2" + "multifeed-index": "^3.3.2", + "thunky": "^1.1.0" }, "devDependencies": { "random-access-memory": "^3.1.1", From 2ae73cba8c88335da8ea594a7f6bf968b495b616 Mon Sep 17 00:00:00 2001 From: "Franz Heinzmann (Frando)" Date: Tue, 22 Oct 2019 13:53:25 +0200 Subject: [PATCH 05/85] Fixes --- kappa.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/kappa.js b/kappa.js index 83d82da..22422da 100644 --- a/kappa.js +++ b/kappa.js @@ -157,17 +157,16 @@ module.exports = class Kappa extends EventEmitter { const flows = this._flowsByView(viewName) flows.forEach(flow => flow.pause()) + let pending = flows.length + 1 if (view.clearIndex) view.clearIndex(restartFlows) else restartFlows() - let pending function restartFlows () { - pending = flows.length + 1 flows.forEach(flow => flow.restart(finish)) finish() } function finish () { - if (--pending === 0) cb() + if (--pending === 0 && cb) cb() } } @@ -238,7 +237,7 @@ class Flow extends EventEmitter { ready (cb = noop) { const self = this - if (!this.opened) { + if (!this._opened) { return this.open(() => this.ready(cb)) } From f298710ecb89a63ab330774d817fbfb15932748a Mon Sep 17 00:00:00 2001 From: "Franz Heinzmann (Frando)" Date: Tue, 22 Oct 2019 14:18:34 +0200 Subject: [PATCH 06/85] Add example as test --- test/kappa-new.js | 92 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 test/kappa-new.js diff --git a/test/kappa-new.js b/test/kappa-new.js new file mode 100644 index 0000000..da3f39c --- /dev/null +++ b/test/kappa-new.js @@ -0,0 +1,92 @@ +const tape = require('tape') +const { Kappa } = require('..') +const hypercore = require('hypercore') +const ram = require('random-access-memory') +const hypercoreSource = require('../sources/hypercore') + +function kappacore () { + return new Kappa({ autoconnect: true, autostart: true }) +} + +tape('simple source', t => { + const kappa = kappacore() + + const [source1, pushTo1] = prepareSimpleSource() + const [source2, pushTo2] = prepareSimpleSource() + + kappa.source('s1', source1) + kappa.source('s2', source2) + + kappa.use('view', { + map (msgs, next) { + console.log('[view:map]', msgs) + next() + } + }) + + kappa.use('other', { + map (msgs, next) { + console.log('[other:map]', msgs) + next() + } + }) + + pushTo1('one1') + pushTo1('one2') + setTimeout(() => { + pushTo2('two1') + pushTo2('two2') + }, 100) + + console.log(kappa.flows.map(f => f.name)) + + setTimeout(() => t.end(), 1000) +}) + +tape('hypercore source', t => { + const kappa = kappacore() + + const core1 = hypercore(ram, { valueEncoding: 'utf8' }) + const core2 = hypercore(ram, { valueEncoding: 'utf8' }) + kappa.source('core1', hypercoreSource, { feed: core1 }) + kappa.source('core2', hypercoreSource, { feed: core2 }) + + kappa.use('view', { + map (msgs, next) { + console.log('[view:map]', msgs) + next() + } + }) + + core1.append('one1') + core1.append('one2') + core2.append('two1') + core2.append('two2') + + setTimeout(() => t.end(), 1000) +}) + +function prepareSimpleSource () { + const buf = [] + const listeners = [] + function createSource (handlers, opts) { + listeners.push(handlers.onupdate) + const maxBatch = opts.maxBatch || 2 + return { + start (state, next) { + state = state || 0 + const end = Math.min(state + maxBatch, buf.length) + const slice = buf.slice(state, end) + next(end, slice, end < buf.length) + } + } + } + + return [createSource, push] + + function push (value) { + if (Array.isArray(value)) buf.push(...value) + else buf.push(value) + listeners.forEach(onupdate => onupdate()) + } +} From 404c78fd31ab8c273b2755baf074face342200da Mon Sep 17 00:00:00 2001 From: "Franz Heinzmann (Frando)" Date: Tue, 22 Oct 2019 14:24:34 +0200 Subject: [PATCH 07/85] Cleanup --- index.js | 1 - test/kappa-new.js | 14 +++++++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/index.js b/index.js index 85450af..8b40bfd 100644 --- a/index.js +++ b/index.js @@ -1,6 +1,5 @@ const Kappa = require('./kappa') -// module.exports = (...args) => new Kappa(...args) module.exports = kappaClassic module.exports.Kappa = Kappa diff --git a/test/kappa-new.js b/test/kappa-new.js index da3f39c..45fc9dc 100644 --- a/test/kappa-new.js +++ b/test/kappa-new.js @@ -11,8 +11,8 @@ function kappacore () { tape('simple source', t => { const kappa = kappacore() - const [source1, pushTo1] = prepareSimpleSource() - const [source2, pushTo2] = prepareSimpleSource() + const [source1, pushTo1] = makeSimpleSource() + const [source2, pushTo2] = makeSimpleSource() kappa.source('s1', source1) kappa.source('s2', source2) @@ -66,9 +66,12 @@ tape('hypercore source', t => { setTimeout(() => t.end(), 1000) }) -function prepareSimpleSource () { +function makeSimpleSource () { const buf = [] const listeners = [] + + return [createSource, push] + function createSource (handlers, opts) { listeners.push(handlers.onupdate) const maxBatch = opts.maxBatch || 2 @@ -82,11 +85,8 @@ function prepareSimpleSource () { } } - return [createSource, push] - function push (value) { - if (Array.isArray(value)) buf.push(...value) - else buf.push(value) + buf.push(value) listeners.forEach(onupdate => onupdate()) } } From e029dff2ae3823a034536c3a6cd73199ebf27fb6 Mon Sep 17 00:00:00 2001 From: "Franz Heinzmann (Frando)" Date: Tue, 22 Oct 2019 14:32:43 +0200 Subject: [PATCH 08/85] Docs --- README.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/README.md b/README.md index f4b10d2..1617541 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,23 @@ # kappa-core +## **kappa-core WIP rewrite** ## + +Make the base Kappa core dependencyless. + +See `./kappa.js`. `Kappa` just deals with *sources* and *views*. Sources pull values based on their last state, views process values in a map function. + +`kappa.source(sourceName, createSource, opts)` registers a new source. `createSource` is invoked after the source is connected to a view and should return an object with at least `pull` handler. + +`kappa.use(viewName, view)` registers a new view (same as in kappa-core v4). + +`kappa.connect()` connects sourceName to a viewName, thus creating a *flow*. A flow can be started, stopped, restarted. The flow creates a source instance, handles state and pulls messages from the source instance. + +In `index.js` this new, dependencyless Kappa is wired to multifeed, creating an API compatible kappa-core. It passes `cabal-core` tests! And `multifeed-index` is not needed anymore even, just the two very simple functions in `sources/` (and the hypercoreSource can be used on its own also). + +More docs come soon. + +--- + > kappa-core is a minimal peer-to-peer database, based on append-only logs and materialized views. ## Introduction From f5aa2249482a23bec14c141d7db45c3090d0a6d0 Mon Sep 17 00:00:00 2001 From: "Franz Heinzmann (Frando)" Date: Tue, 22 Oct 2019 14:35:49 +0200 Subject: [PATCH 09/85] Rename method --- kappa.js | 2 +- sources/hypercore.js | 4 ++-- sources/multifeed.js | 4 ++-- test/kappa-new.js | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/kappa.js b/kappa.js index 22422da..d5f2392 100644 --- a/kappa.js +++ b/kappa.js @@ -303,7 +303,7 @@ class Flow extends EventEmitter { this.kappa._fetchState(this, (err, state) => { if (err) return this._onerror(err) - this.source.start(state, onbatch) + this.source.pull(state, onbatch) }) function onbatch (nextState, msgs, workMore) { diff --git a/sources/hypercore.js b/sources/hypercore.js index be7856e..cdfacd0 100644 --- a/sources/hypercore.js +++ b/sources/hypercore.js @@ -2,7 +2,7 @@ module.exports = function hypercoreSource (handlers, opts) { const maxBatch = opts.maxBatch || 50 const feed = opts.feed - return { open, start } + return { open, pull } function open (next) { feed.ready(function () { @@ -12,7 +12,7 @@ module.exports = function hypercoreSource (handlers, opts) { }) } - function start (state, next) { + function pull (state, next) { const at = state || 0 const to = Math.min(feed.length, at + maxBatch) if (!(to > at)) return next(at) diff --git a/sources/multifeed.js b/sources/multifeed.js index 22cc78f..fc809a1 100644 --- a/sources/multifeed.js +++ b/sources/multifeed.js @@ -3,9 +3,9 @@ const hypercoreSource = require('./hypercore') module.exports = function multifeedSource (handlers, opts) { const feeds = opts.feeds - return { open, start } + return { open, pull } - function start (state, next) { + function pull (state, next) { next(state) } diff --git a/test/kappa-new.js b/test/kappa-new.js index 45fc9dc..231f2c6 100644 --- a/test/kappa-new.js +++ b/test/kappa-new.js @@ -76,7 +76,7 @@ function makeSimpleSource () { listeners.push(handlers.onupdate) const maxBatch = opts.maxBatch || 2 return { - start (state, next) { + pull (state, next) { state = state || 0 const end = Math.min(state + maxBatch, buf.length) const slice = buf.slice(state, end) From 44cdd21291eec07dbc896b1d0023ca6fc3002564 Mon Sep 17 00:00:00 2001 From: "Franz Heinzmann (Frando)" Date: Tue, 22 Oct 2019 14:38:08 +0200 Subject: [PATCH 10/85] Add flowchart --- README.md | 2 ++ kappa-flow.png | Bin 0 -> 81862 bytes 2 files changed, 2 insertions(+) create mode 100644 kappa-flow.png diff --git a/README.md b/README.md index 1617541..320c95a 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,8 @@ In `index.js` this new, dependencyless Kappa is wired to multifeed, creating an More docs come soon. +![kappa flow](kappa-flow.png) + --- > kappa-core is a minimal peer-to-peer database, based on append-only logs and materialized views. diff --git a/kappa-flow.png b/kappa-flow.png new file mode 100644 index 0000000000000000000000000000000000000000..1e634bcb0ea984330b7eff17747591698a227dcb GIT binary patch literal 81862 zcmeFYXH-*N7cOeWE`opp3MhggMUoH#q96ehN+9%vPDr7okN_zlMPC~oQMzEqMhEE# zC{3xN0@4u#l_DUbbk55Ae&vii&KcwWx##@3V?^0|uf6Ak)~THW;L4T5I-Q#~?QQWl#QW2E6fYWilN?O{pCdV_nx~JS zzZ}d!4hrRRxvC^z0$0_`2b_T>x(~^V;zcI?(+pLEXsJNdRG?5B2viQH2h#xm!6B+J z7~J8X`w1Ro#=m5l`2>2=XaqSZ3a+XKL@D4Q5IGnU9AUf|Y##W_)6{U)P*(&;dQ2vb zj3c|7dinG3hapsfXz+-sk&PKf4yq51X(pflyUbg=uQQ zRS|G--I_ol_jSM#$c1NzF13BKcbO7kL3U}{zr8V+1Jm@hw;yC z5U48X);|sY0Zj5gS4m`!7ZJRZFOmtm>i4gk`Bz|uz@#KEf`>1G4w}9GWip(#pA|=w zhk^NNacCA0qA$)I7=tf>K>+IiZFLJEN(J(d5QH5ML)7wv8d|7B=u|3Jjf_Nkux+eJ z>;SqMl*&-k3{VTSGPE|frg#!8IN%I#4I`jY-(=G3^cMO+R*9Nrry?8z?dkO zzZ#B6_VD%afRN0nFfQ2^Zh)sS@#<)zFOXy9fHk3j+w?rxcBU{-sx`QX@}%3~>=>HX z?o2O^FV9{LYKRD+sAGH>-o6H&1Pd)F&rFYG!6V~5;UrD~*UA{@Kp+$S^zY7u+(9FkZNSDFT5A}qtNTOPC^bWS zYkxySbxV>OA`r&aLTK7)K^;tZOgI6?W#MoZC?t<<3<-c6lMn$WSU;p5)5g=tTOY>o zG6VsJwDb4YvxL%o?aaw|V_RzzbO6RwljLFRWx>-^XZRR0{Ro;wTQzGB4;J0rLJwl& zg;pcka&0uY{?>u!dR`8JT!@`H){KhN#AC3g7*C2dPRq^~%?7d=0cPszP(3X#4#tlO z@i21tHCOj?=bD?**hEhPlW5NGn2`@gQ2|ORTG0~4~ZR?KK zps_tc8nQ7Uu+6#d4iIBcLt8@_%Ggie1EOc^t%;zSu-p;eftCh7G#bp*2kp<%@F8>k z5F8I5cPx|3wJ=4SnPMpp9vTK53db{$X-U`e^YYNLx6v>)x3n{~^}>>2R9`!ywJnaw zbT^=)4Sfs^s6L)%1|(}Wj)SJRrkNVmTMLQdk=)fRL5~p*a2j3D!XCjf^0PC5`$N64 zD29z6*2e>79RQ)gF#%Y2cL%y9nr3K#SL2( z2c+tG1{kOl_(5#|d=werk2KP=R0HD$if>p4eVEE$9Oi1b!H6($bVriN*OFuHgf@rJ>&e(ndG&O5e5}AecCek6MIJ^-D1+Lrh zj7?3jXp$btTS!cxrN6liP0QQL6mQB!TBrfBNVo-0GXP^~2K-XbkK>1MSL69ZDLiZN zM$n4TqH19X0VD?-s<|CjT|42{@^-W-x1+TYU`fnwRiFj#L>bwhW31Pw%%KM7@u#`~H0qBRXr<_J>) zlBdS?@Mme7S#rFAEO(L#Q5|jJuW4zZj@Dq}ZCHj@8gwlj&4qE!xRsqIQ;SDYW8(D#SXeu@ z9n9a|lK>C!Cz!)5%t;^fNI) znA;gp*#q?q|mGx3afpn);(441I4p z1IMw{AVBPha0tvMkjoAvKzz-SwiuS5hn5FK&5MY@;moieK3*1TbT2kWOHGTXW@{9n z?m)2eqgmSHY+3$fy1$yCF&)VQ*3_~wHzQiWiJnA$%wUi(Q;Hhh8-vu+3vl4jX+BU} zL?GTbkmk#eT898U*&b`fgSZnc?9DAr)lD?i+ye~NdG2-=9#Bgn9p)DRA(PcehIV*8 zO+zgQOA5u5hOqEwGaSsIEG7r-z(XKWB)p%E7gQf*j`AYe+rv-}#u`{lG6)5b|3T23 z7{Tq(Fn1!!ocZX!tr9`+0Fa4IqAMcJ>a&z7U){1%xO{kL5{+IM^H5kj<%T7*l^U zDjv!}`vW-yyGXMwbZxl{R1#vW)m zoj_OjGI0oCQP5Tn?l2qvz;ER51x6qvBMVD+w3dgp5ygywaHlaTc+kcp>%n0r2sV<4 zLvtYnGSZW)X3wEJ(Cn;{co-{TiI~B={fGJ75qd9IhI;3w2WH)wg30#&z3&MoUA8 zOAf|hGZXU1<@DO5r^Ug|LI?i)i#*6P9`T+Nwc-`aGQV~m z?SIMc=CG99x(|klBjnxdqsNvnssFVz?BY(I!shVI>#A9g$8@#`RfVNJY(Ogf=#kx+8sTg^)snf%~MFPT`g=Q_l?SjsDv*zG!CvtueeHP;6a_n4> z)t}sQ+>H&|_%aHQ0s+&B%1v7mOHtz9`%Y{*K71eCE>)hFnz*vl$Z4Q)a6;D9DZ2_8 zf!$_MGXdK8GCZ__fIb!b`0dK9Xy``?BhaS5%gE`RKLM`tN9aardF;zhbYiJP@Kuq! zpckE>jW45}0|XR17AWk&k4TW8!f$}K>%uom1HZlYcK9JH!(vNc$c$BH6)o>p)B*O& zJb2?D8Ph-)dI`x=EUymUWI>eyZGv(#tc7=1J74sp&yP&#{I-*}AMsugwF){ev&HVD z%s(>vfG)j^w-1!v!m081yUv3)DH9o|k-(o{jA;YqqL(gRD)s*^ETt3l?LJ;T`|0u7 zpUtObjc=ZK6R&u1x0rI+ukRnwo$pj7(IXSk7sWI95$_ebkJ}8+Z|ld%w2YkL{%|^? z$=sx)qw^+B3wBf9EW)$nL77(2J|FHDL8-InEi4Y~KYHeh*=>zu)UK?jxZFG0Pvlrd z3qQ-$dEuF+bI<)iPfp7mkY_FY`GbCaW!LA)NhSM*zan2dTR*B(b$GF}JJ%Im!I|zC zMLlXvC`C!!Z{&b}f6C~r14=vytJzbZkkWPG&yV|*)>KQ8o7d(zTlR=4pN~2Wq1_JH zmsz^L_Giem^S)@v>Jqwt>1&K)(qV|_wx8o|RMkt0u*Yt%zwBC^Z`X~35K+IDD-Sir zpSrAn>AJ-|{N`WZ2X`FpDy`k(3{qi>_5*Zm%uz|M z+d^z+hmVDBnr6V3x3^BBxz#_O&Jp(YzvLbY7WnmQ_3Hd9!TlK~@vmp~YUg@0bMISs zo9|4Px@dhJdu)yxzTsz|<6I&ZEbv=z>Qnx!o3V2b8)S*sqU5V`UY3uHx85%G`?|k8 zsB3$|P;QiLW6Zq*AM%lb(2u{0$bDk|Q}2cPx_oM1*nh|)cJFtp@)ZvqT9%xrv|n}= z)SVhAIkeHgv?Kd=NyC$UI`z?#h<6zthmM>LI6X<^2#v7%dD$=ULces1@Jd#7jN=Bbs`Qz5!buL-ULmm|) z9zOT5Q8r|C(X;#6(FjpR$`zmQKQmCO7r2wrIG2Di-N_m2rXB0Dt8v;MjFxo7p$mdj zhEF)@7NsOY%iiQ&cab`p^L^qXiq_j!LVIGzl#)iT!*$)-erY{Ey|HUule3yEUSU`v zw)3oTnB06)a)ox0PR>=DeZl<;Nk4X$i(EmnP=JNV#Cw&a9tZda~q}H`{kL`3&p%(s_ep*kqYwft=}Z z`Rvr>!&Q!p>2a%w_f?mUtzExI{*ajbr}b>m{4@gptMj-`rn5>Nuz%#S|Jwhv2#(^o z6nb3FG*LubD9mchWLGv!ZYw8?3!;NDR5lRS-F*6G)Y-E6!vvZ|?&g~(O=BdaJ8R1C zc7KjN7%Icb`lJ~=u3j+KOu6zr}Z3vf}1QtH`I9 z`~A|eeiHj+bo#Hw)v2XI;_vt#`OMVM(|bKXJcwShe~DVa2$X$B?=x%QE!KQiu*|v= zH}IF-?o^CVR&u`7AHU(@&GFA{1j>BB1g^eR!sXn<#_Eh;Nb#dIgkL_{5?c7?QBcCq z$FZhKrwMPlrpzDxTK(%^i{C!hpnnu;l|bmnY|)AeIyA3HDX1ut{MhjM-1WKlrl(Vs zRdhFf%(2w2V>Kr!ZXnIEQZlT4hg6R$|LxPq4tO^dd9>g0{P`YI2OyA}k6zojvH%D0CL%RxtMN(jzbYhEgC>1RE@On>}t zeQ(pxE_gjWv}09XvL}OhXsV}TM}3p%1rsMl_xb9y4I`(cJ-y}wX9zv7$qFuyG}~M? zeZJiAbu9L^ztwTJU?xl}abrB&Kf=*7x@2^;F*0_~q0??7@!|Mi)5mEpCE_o4{6e+S zv^pw{{rr{_GI2K)FUTM13Ed`|Xq8$f(g`|Q zR~n)vM%OQ|9E*`0oHQJfaB)B5!XIJo$b-aD?AfuU_S!z{l#4m33MXrPH|y%^2F=wU zvee;_cI?e8g*v|ddGS^74#^a1-!m1?%s})|%=)#fk%t}x^o@@v@|e$R8dN0n8TDr# zVN!&@!=9k4&Mv*n_0YYwKuRCK1<#R-NUZ5DtkSS1Ov{^Y_x;*LujBT%czwzX*vg>~ zlCRLNZ0!6s-O>H=i3aWcfvV0RvxfMR-zDn>qUXwgsg3Qz76bXRX3y2fIoduwUr)vF zA);g8TS9()v&7szJ3#WW-*Vt2=SY+!ysf`{XnEewy8PW>?m4m3&QXPpZSJ*?_dO1} z;ClUx~nI0&W4OlB*aR$Jy?V~POezIs!LR=|17K!nP%Q zBVtS`RTb}{O{zJtDDq41b_~6yAgcRT6~ko=L2Xk`(`8HPqxQa9knA=yQ*!+Ax(pJ~ zPx|sUq4BI6D&X;K!BuqzFZW@JHZ75vJTP3tVYz^^uI zogYSkl6OC-O21}hG`1RyOt4HR77pGl4P29x*#u<9oseNgpE#fX;|zFDcfM`=k7J-} z6y!#q0;Q|gw&N#qKuPzlujF*q`f|r4=c7T5C@48|?mgETFb3L~FH1OrTJ<1!**Svf zIsg5C*T|>uB-V9C9&)ulv{{I``;t-Q~L=Y(T-j&q!h-j;Y3M={Y;HZ?`mZsy#Gp-r}f=vY$% ztntdnp<9I;D>K!tgTMRf;VPgQ^c<>sx%B7HP-yJ-Jz~ia*Wd-)`L&PFtcky;dP;+q zpbBW(YU*eSy-zIt3>&&q40B;M@j1rwuEa>%^q%B_>@mPj9{b%>^S*#p#v>_ zpEoyRZhx4)rBLI}suV6epI?dx4y95XfaTic4k)rRc>AT!9$s5s(7(1wR5iX>iTO&8QHC>`Yv4LrV&5ZY;^w9T@L^ zgwh|zOlM|N!2Q7K-lC2QuFsdZw{}#oFDo@fi4zTq3ANWK>)+pJ)GkbRzxp%i`vek^ z@9>51DeV^L@R7h%947C3g48&LmU%tW)G}7Ru^t&As+78y#H?U`-op9OAMW?H?Q_kg zt*CFH9wyhp6%=;TdtX4VLmiiZ%EXQbhA5}%wcn0)(URS(QOK#$QR4FcXVoVyH|83p zqfj}4%4jUhxsnG-9wSZmmlek8 zGR(xSLfdnWHVgBpp}^zh3!CN_;nf?DO07_hF-IeT3c7MR%EoS$uQIlXI1T>R4O+~5 ztDvixa}s+;J%vEwDGt7h&e)+G>{e{A@)HC`oX1nCEW`67-q{Nl>cB~U%N`)G_Jw5D z0JWmhehL;o_h?IlAe(4YZ{lS0xic={b2hF<`I_jF*1p|1rhnk1`H@>ik4i(9zdw0u zo0+IpZ5Szr;v}7Rm2waLGc$zf`Ggn>`tU{;R@(BhAb&9Ncx$gs;_hwRwk2C#0zr~0 zysZgJb+va_V~rSCKQt*LE7lZ_G)2*Y&$px%c(m{D`t|!;M{T@h-g<)EesWX%=E5pa z#Wy@MzI8+1Ok%}(msr%NQzK2(6nG1P&Shv#RQL?+oBVvHEfU1|OV50`Eyz?!2f|*I zLQh2;fkkJTkyR8nV%I^0ze!L^ifadTVY1alAT#xDe$Yj$r5=yFpA&E{kddaom8`go z8yfUpk*j+SX$U%A{V1K0AgG`1P_$c6T1UO;ar_`Y-yM8qk;k0G;KGJP_h!~Xd5ou& z*q$~47}4Z&@MtvRhL#`Y;5_KllBM0nX)IS5-@f`lo)s)~Y-X*juJb~-3yhzZ_Fvi=PBpkjX;_%*DI5=9^N_4h&<&j~ z>OTIysBO+-i=9xnbm(eF&|_U zJ6&CL(npjfCztqQcHco-$3rwa=7?5Qjl5~%lH4)QQeKrf2{RYcclT#@VFS8%J29buYe1 zd{TVsSTZwGldsE~YB5wleXHZ<&r__T526`(Fl1*L0cWkZwZvT8R9^(FZ=S&#YZ9H; z*j05qX+*qBx38EEgMZ4pyuYoyKx*aXpTP?gZ04#xzK-2d3=0$cDk}2k@s6dm@|u{o z;%VK>J4!aL`D~udPbfWep{SD&Rg~~kW?w{~XO&2IIeYTVIrplCV<3ePe*Af#VLHCq zK<8p3bn|*EE+gTz>)_e=McSUo(%-J7g6yI8Yz#FScWi10bbh%r!ugy1xeK4L5f5bL zaxAww)beewyg8hDA}LY4VpxSH+mnD!`+W zKAtl${sY-pZg^Zcrft{OOZl$!x_g0TwM^1SspG6@hd~K5FqF;jG;nZQs)J8%NWi_n z1o*qC$yPm&%>6MkS%RbnC8==dM#NqO)m+Cuo?pwo^Si1$Z+1AdZqeMS3irpb)nue8 z{gXmAp~ne1viu-{z1A+5ljD&k1ahoF&CzYg<#PIe$4hSCE2*LEblQ{KXpnwR9&u#& zcg|4oALpmeh5K(}@2)H@UFv)DlgfBg^QT>>B08&(7}d1?x`{a6l0q5X#n700KoMmH zTc)+IJMpXAHeHvIiJ-?5mi!_&>pVY`XWNwZiI!Y2OE@K(sOlUMyz)E6Ilr_>N%}8w zi2IeLRIG2TBw~O6)rhaQ&#bn?y+5i6R?Ncn`&|-dcw=_Y`{9w2qG>w8y}z1O$N?)$ znYmT3+jTc+gBdC6URLvK?-<>}s)!S_fBrOroP&|>pOpavu>5Fa757KK7pK-e`}pYS zeutpNYm$<)_hs(XI+qkP2E3a#3(J|1G&J0zWf7!`G@`Uq9(Mn zvDz`VPc2?Wy=~ohSSh2};y&`4k|Zp-%@w7l1|c9Z$mQ6ygtacq-%GtrcE85jC6U>&L&{`@t4-%p6_aC2VMpEH9J*t|4ejdQd$=c z&J9(soDgL6G()r+lOe=Z9E$bQy)h>GZpiZ6EZNZumbXf)!fu2}34_qi*~hQUW-(iY z#Be{1>K%t)1};EJr4{eO51x!t_-U&zV)5e<$Q&USq*D5+5}OPIyKIL}qgL>$RsT@!;jDYl)vTD;BiHcduSBJ$4f)qS{ed|Egl@%#%q@OSr_M3_v z8U571u0d>wS{%MC*zzeW>Cw(&TF?6_VU+!NW+E7SDC@I_Bfh`8uOFuq94Ol{Q@v5u zh&}7*gLdE=3rL)5uG5-k^0E%N3}W0_%hTnc*yYL zms{8t35yr@gEdROmv`zss=ImMhgidt?+Bkk+VW^A_NUAT(C^#M%{*#)%hIJwj-x+% z3LCSe+XMb(QdL$bT!ug69W>6CO~NQ-8HWhK7E{@(e`^YHETmShM@Kv5~)i87<` zNJNXVy;+$wQWtJ^ElwB>xeX;p^dyAcG<ZrGLCRsePxd5xqV_6m>>1 z3adfct#~E=6osbvw356*NetLCjhrq9?i`kOh zDHWT1$ZxI`r(bx8f3{mlqC4ee_8LKai{KCa$@ahp!G-0XLm8*vKW?ABdn}&CmS-=> z@vYl+43r(@0eQJwC6`hNM6rYdBglh1H7ff11DPD>imouTpI_dZiP!v|?u&i_B{n&> z_7sp|8JkxT^-vQ4tZZLU zM>^JnZ>!O=5bjZRD!q6c;Ui)BcxQna43s#iyrl(D&R_)MZ<;NQ*V-McZ`}Cs2@%SUuE(#@C+**TjPj_`zzfhHAV)rVAk#oBHYR_*ssVvuX zq3e}zSEbJK-jTxI!teziEyk|I*~sdI*c1Ls@J1G%QFfgz*i zqH#a6xwH0vF2%G2E7O((;JxZOS2L(w9h|VN2xgmkIcrT~!dB_hRi%>Sc6=D?=izdW zXH;%d&IZ;;xO?ZOE=6Gc<8E8|$%4|6yfB*W*A?;e+0t-dwMb`z5-yc?a#CIKaf_9e zCw|Xc31gE(R9!_~5et)c+)pZVd$r{jnXD*ax|z3s zEHxPNp}E}7mrPXL3achzo(Z;%+&nkrR71R6_^_qe%3FV=0gv}-_EQ{;imfb_I&wF~ zt1VqlI_h#1{@Q_-%X4Zm!@~Qzv|+@RT0z}pLHMf7>lRj`vc1dVNqmca<{DL(-eisymZzE>ndT2)4i$xf{slDCt557%Y%Y;yz{b8oI8#>RXP4W?bDk*7iJglb+js>TMTZkrrqa)>-;V!WX?b3F5u*&F++y+?mZnJj#Z0P zoP_0M>pY7YiE-S3PXtG-#6S}@qNxvTmQJDYf2Idx+WXE0o>#+emj2R)`*f_>_Q2_o zEvxL&GfkpxQTf(iWw9aBX=kOvdJ`fEum0T5bN~LAMLj&^FN4Ecaq!+h9R>GK7mUBT z%n1o*uJww3%))PjiDlK*+&QWrm1z}zgrd5%XKjPwowoA*V51t-rp7wyR^5VOwexZr zeZ6`|2q!1z}W27|Pn;J&V@1BiW+~_Nsyd=9vT#0i; z*<~T}?HxwX>_gf4z?wL-b1~9Yj#oCz%HMa&5R{J0#O6ca+)>^9{?Ua!r?u;nrllJP zu04=ze%N?%BRBMM{EzUj)?AwHZNcM)&tJs0+b<0uTT-E7^NI8u zrPs!iOd5d|!Y;P%GQc|pn5GJqfH5iKdiuzq3=@A(O8tE(5v zMT99uDXnesiq^^Pe>7NhrxM}ki(T;0Pk*W2L4=;Zqr2y8``X;1SkT%8*`g;dJlpcm z!Fj*L?W@6)^1>$rj+#x_qV_hXU*4UNDO;;ObKmt(@zbHweNDKZ!OA&TBNI`@FXr-n zM>jt*UrGFa*&dsCGEbUv4vk5s7gW5;ZvUEnv}fK^!8Qkq+CO!zKC?u3UA(ZQ<*&i= zr9-NBHV(eL9tG1Kt#w;!g$BX(CHQrtAHs zBMp&0?k*M6J6*89$4B$KbDSc?Z#BC8xOw+Zc}_fQ-+XS$ei8mcx1&&4UTLp);_j}* zZIxzD9vF$}R>>~+_gQkhx0dc5l_9nwr{{GT>+DfQ*4Xr%sHvD+Tgl}4`wwkgsyU*v z4eiK8b=KV4aw> zKozsLd|~fo_gERLGiOM|)W60SrwyS}i|Z~3*nAui!cTvbjmI$5F%MEzODmtmLnGT3s+4<@p)6Acr;x^8u z*=|n{+zS=-Sesk%+RKXHx>^wM@@=yG1(=+7aQ|U52eyXp&{DI!wJNi3jw!ai!f=H# zPEe9sv2TuLEp`VR-GPc0d~J%o9`pX{*YL^u`L9vTjrBgo)yX$+5Z;5EtBNGgK#RX3 zqap<_Kqfc7)Y{+unBykZcFN{OJT&LWvSDW_zp6NJ72r#jJ010}#)2MG7r*g_I)aYc zO*a`HT`204e!0InZhE>mKiuW|Sa8x`6tJ!|To0absa&+XTVB^Wp(L^}m+eq>7`-*r z@V5EOBV&8m@49kJ6eD@X&unBSx>2$KDh@kubKpiPYCAbv=-3ue(GWfu?+3uJvjAU% z;goGi(QYvnhh%QxPv=QSLoZr2hWJ)agZm%;)Uuhq{M6?zd+R{Z-Vim29J=_Gq6J>K58y0!0 zrF_t@5>WsEQn9-cujfVnCJgLt%rWD%6#MQ2xC0;jF{Z{G*H7(^gX{Z}UDBCz&<}yb zpYCdM#*e2ay$xGrt%rU+ebRp>UiCzd`KzbqU@Bjk%qJtzR@c_d-d@bj&4uQ=1m%|o zPPDuLJcc*%J6}+{+d6B@vNizjv*Sr+&LDUH^){EoM69 zodz<{ZKY(dn`nJ6jka$wXu=i-Nhj7-;jc_iBYT0mu^2ccuky@<}TqdB6wi!6lVqnca=+pd-Is z^>M9W72{OF1Pv#tTEih)Z-pZ-!fvH~bHy`lS@ zK?2FleDmBDc&ZOrGFeDW=sNo_3gFXC7JUpEJ_`u586+w^xYX&s=K=HobO+IVH0WQy z;U)f8gZ}rz`E4{kVDCt>)eU}B{`&_4ed~Wtz(KJL=yW|)(TE?8|Na3;;P4{I5pG zW&RBq{7WylJ>cUyD&P9+?=kUTO5G@(kodQH{7ZS;fm5g?8td^h+P{BBPV9aEIROWs z{sLb{toe})aJBzf2AE0UKPTYe|2NZr%rtE<+?K7)*^SFbK`!B0M8ml-33I;X&Rz$G z^y{*MOv}NpI+zO`HaIG9c=|N%p|^4^OyUqc|y$ zXT7dS^uhoUM*Kvj!H+;NyxdXB$@1E_bLV-mES%sKeZ)2W z=;kdgmHYo^rq=?1GEtfPvu8)!bw}ECxtR2sFg@8D2(F7hy(vmDSuJ48`szHE)RHXp zQfKBWG^1(QV)&Sdg59Rz@;`cw#Yb7cK3RU5dwW^x?w^aUJ9JN&ChZOI9e*_wx*GGX z;~}xJ;V6D>)ccp@N5PZkcg_RYtYaWo+dBxql{+(Fcz1KKTIlDX8yB7lXQ&0eV^;p2 zpnm&Uba2b#9m5|#TN|x9jBI3m>19(pJlv$~0Jks8R!Z0VER^!bb7ZnNQ;9KH8LrL! zaj)6K*~=cyIDhK@C~nX6+lU9VB~Xg+;S19Ka`Pn$Dc1NdTdf3m9~KEKZ{ zF3i>r249$KPY-=p9zYH19kvE2@mA+R&Z}3oJLYC;LoIXswO~HQs8)$%&Z+*Thy8^b z&5!7(e)Kw^rc=|BIdiWHUqne*nz6rGXRJnO+4jgJW%uR}TV@9$W@hb{OPH&5thzAq zt-V=JiP8pSVif@*jehlN;#n5p3R)q;d3E3OWSzYUe0mOT7Ebf<#` zJv?Bjr4}(1a@o*T1zFVFi9CWhTP^cn(;q;wrh5ucf>o+3q_;Q2Q+AeKty!A?3L!S^ z{WUW*q%w-8yvvK2{GHwx&3n1}zWL29?O34(fdvVffPybi=PdVStpprh*PPqo-WU=6 z!NXkEv5JOzI?iYWTOyY2VnP2Hz zJN=d!z4xhNDx7ic$adeuORD46EhH`#@ie;j!v4{WBp0Z?L&&D5&JQ_Zqlu@j*RQ7C zIeUIYLe=REokpTS^JhA4V1ee;!FSt7HMUf~oc#Lr`su30*K~-1|8QP}(#lu* zx?FQoa?5yPw&M*2%j2@XSy{P?)C1j-}xOl7VN=!0w$!E~y zN$J%tp0-X%g#d#?T!oQeZI6L0 z2jsc6ZFz)E8m!~|UbKg)n!<|d+<28BNb4`l@3khL{ZAo3bW5d208tF}U!vHs7swmk zV8%oMOIhx6TNrX+ZE6SR&e@)9hyiI97`HG%S@3W_H!rTNIK9{X&)~q~^fw=TDeqWn z37x?R-;uPN)?XkrX_uwn6t5Vk;}`g)`Ig{X;BS~cw#!S$e$?TC=dPf0lhlJcRD}Jh z8`?OAY}E&fdF{1U$vo=3sw_!%6@AYqw(7A?5#iRg+3ayD^l@cT^+Hniw~mbKidbF& zoY8aG%PIp>(ihOIgS+o^Slc&CX!apG)_wWQclis=!WT93N8S3Lvc|jCy6%>wzQ6iA zWICVs-97CbM@&+Kt~RzugsRb%8-icapBl;^B0bFP&Wwo|J9RvCaP{do$BsI)GKyGA z&uBPNK6QEE`i71=yPBpPV3cqw;aPTu+KwOl)H60+M(=INv#e!)ZrPqSlqXe-61 z!svS3L;OCTW<@dbnz+35W8y}`zL^C;(mf41dYWH7^DB9cW5eG^0w=pv<~O~MkhZ(H zF;4g2P5OQ_qjk60-sG^iALMUeFB}Z`di2-#uEw)x>os_bZh5H0k`-<4H`)o+6Rfd0 zC0uUMDpRaJsmEbRFerL)?z8fsNi}CJ^_kF^zFo`ewcpIH8IMr(r0=ecoR3h}N>gWyb=Z@j+b6VMs@rqlfIL@9!V$Ti$)}=|#Y;w>PY$mY5Q9 znx&A@a@rE~)>5`b@s{!>Hf%%cyRLBIcpDVz^oS|6cS`#m=~1Rlt~+B5r|jC{|LRQN zv8~r?p4=1c^`~u3Bv;<~#hzE4u=k^UUVS!y80x+=XQ-6n?aKsJtvT=j`}q zSQG4OlFB1i?Mmi}|AnQzZ;>sM%>f;beH_CEt7yXs+iXq^Cp0r+jZK}V^UWimQ=m8@ zcx1@(Ck(>@{7OYt*%VZzJsFm6LVO;bjApt#-})P%*zL@4vj?A{Sv7h~qzs@bY zE>5_lTcFG7du;~F(m|J}P48{u&$Y~DkRv8@N6zxpmKP?k?-hp*gC6lo24Gb%dN|vu zOt?8w1y&(t>6E1E>|Hz)byT}}>GsXu4AVohIoXaSsDx9t%+ZE@A9G!Y(!d^!jVqsq zZdKy4@RvYiDqU}>z%OR#k@3O#!RF0d1(K7xtsQW=F3~A4kCEon)pcOx&=Yz%mF>!e`F+5#?09M%v<1B=D2s~<8q2|ia2ZE?%l}S8nM4mdKCWzDmQ{MI;u2e zWx6Cx5|Ohce)UshW8<%*R-xDj>!Y!{n7twmK+T6>Q_T3#hsS2HpMB45GaIktp4$LE z9i|4@O<%Mduq=}26rZ<@>o~SI&+PEotd2U~XaeGuFkP#xl zGat=O`gfXDz`A!BD74>1K3gh9_uDU*->V1Q+@9G3E6ItYIaJ;YJ z(K95Iy*$agBjsRWT&?+SSDBxOY-oSa$w%vVoMDM_vt8?8p&3I-_&P@o*)V2hM8zD{ z76WUu>MWg6{ttsLW8wc4fv#n6ENaP~HCCi&AN=#P^6yup zOTmdp)vTM>b=Mb(@v3W{p#ku(LU|FTQo14iPTGI70HrJ{*@&#`lbWYNCl(&mciDg> zp%l8_OU*TWL`@(4ye&&6C&(xlY^EBowE(o;INi|o zLsm(pRR`j_!S08J-i+=#ASMkUK@%r#Of)R8jV!>~}$;z4H#d zI{3E+T}{pp0mjhFnV^d4T<#WzxEG$$r!+&rR=ydD`N$!+@hVxqmf0 z`ugeN^cT6Z=2OY$=}!hTc4Zyght^iUNO#%muDl%1U83xbP>S8fi0=jgl@d60(h+dy{=9Ae z#5=JwA%d*euX)qXL**xct@DG{aut4>TYWLv__brVN3>8%2q2W+f8xi__nSUn&@`D_2_oppiV?A^@<{GG2$&ReWdm2`Mtj#XojslVPS&0239EbN zCnYp|Tc@GD@5&{%pX2`Ux=vry2N(Hi9R-*^%OH7;lnjOFKSV{-G{)XsZ+5H=Vjps? z7Ihe{pp``)D!DiI;d@Ym*wDs=*UW`|djzXYqGsyGF0gBRvySHk?ND>Q)Zk#3sQLx) zW!FiWLKChCyWs;`(zHI%&c14uf6agI>Wzj4 zqB9MjS_fB-ce)T&*I5+QS;5B?OC$N|((##Zi4x;a>M7e(%Fg8?E5D~<@XJ9Lua?-N zqYe|QC}zWe;9cYVl;;A5CX0uteTAPlaoa|IKM!hDDzO^NvG0F640c35c4a6wI~D@U zx@gGyk_W(L_8H%jhYx+#hU-)NZ`2G8NdtB=ESOn)836LBQPL}eWqwZr3evDyFr6Rw z>dkhR=IiCV*SBc(v#DS^*5e~GmJbXl#I1vWeL-ypoIJg_Zk84UHE)NR` zwRkgnU+e;maJcSC?S@nU_*;~*oDUyF*tv!7lTcTaG+++)Lz89(07ve*eIdyBsefj` zLcim0UrbvZc5wccTn-NGNu2wD)lFfOf(UfLvdwFd3`(nmSJtJ4R@QYcVLBi3?Tf#} zx39LEs+Pn8LqvFd0(HNRk}~PaiD;_M@-cz z%<>_Mzui+_b+_d#Cs#zVfnP=VNyTR5L7N*?NK5b902CA*=10Y?YlFS4w?lTV7~zYka^_oax9Iar8RYyz8owR5TPd`m{t8UBY-da_{5cpt9%yjr^JgMYwdH^8E1bu7P zq-lN>dWlHg9~`KC?r+TT=^@xv^s4z^f(+=iv592yZ;S)uk9)fZZCX69!j(RcU!K0s zi)+6$HOY?_l`b^yPW12BJy)D8REeckNHH}wc8)gWlxwh=<&HaAYvSe3i?hc|yrjz` zVd!Q5qbfUyrCNl~7JCr2b}w@k$=ZX%v6 zakpB>TgtL(t3m^&B457aKvKxaj8Ir1Sh1JbLNp0Hmt26CP^#D3n#D4_JVAf9H4Ufx z8p`SNc%eTU*@pi@vO)!i;uVxrQRk+E1A$~NXfpFm{KnkrXN#O+u0rOruoy7?Wc0m4 zVBPD4^hsAwyH2KW&E}o??=KR0Oe*1YOoKDZ4Rpa)Q&2ABH~}|%)u-jaMyD(t{Bo?} z06?1ZA09m@u$!1LQvtAEA-!d%4g?f)J2u8Bm@{fjlrP5$^Xx>>ZSEV=i2f*N)yU>XztG)YXd2OugdrYi=@#Q#(5 zo!_)L3ft!}u3RR49{aUm46Oy4Z|?%JNk?)X0Zz{pQAY`7RU%OiAav50%aU%2KE+n= zR!9W=ayD&^MB{R7-%TaxG#+Pji50abcOnPdiClO5^%4`y^vTc@0d5}terv|O#5ua` z@T0!}O9Y-5`+tMLYrpka`d<)uod0(acvt@i1YWg^-Y*}@t#&ulfn>ZK+M%u{UR5?@ zk=IQzOi$d=j-~Q1DzgozYI(f2zkE-WOL}NCRlmyK?y$d7ZhlKSZgW`>m|`=(z3xiu zpph$`{It=<+NpE8PZ`wtHGvmgG%}C50|N+J#Dk%kAxT)^Fx`AdHeZ=|+l- z*%E4#>Mi<<+uOBO7VUI*4&LHzI@{DrPiC!X`qLr1b$g{?NCe@}5xrDmp@Rz^=u@u# zuaJSabShtWRi_dMFE>ZG#^|%85)B9if5y`Lq_aJx`%C&Tm zlF#$4KCR!?73keH^mv~?iL4xCqPkCfu@QCW^k0~sWDz+;4x*gM_;9=nj5EW)RuBr8#IOZ)E0bi; zV_CERT_hgy9}1&KHG-yrdX5C&6;eGM-Tx|e>c4jnul$r*o6`1)7xyEmDbt`7kz zku!f93$&Q{-TBs%j4w>PYbIzXl;ulOJq!Le)hJ;Nv{MBhF`KrrETlA||A)ckg{`v*d z^m>i%ylRlrrQS-F@T+Aux$XK($c-|UT*fg(`1TR5j-G(U#72O048z{4gNAqAi>mza zt|l`=5Lvof|Bsx`J5~5)T+M~pjR3iL6c;X`iQ#9ahaFM?j!4)IXk5mF37Z_XjW62PkFRJDIv}3ZCvP;GPlIy}6QYiR!Ac`#IK|$StH{E8si+ zVUfd&ZKv-T;k=_{GZ?u_!TELl*;P{wWf%!sUcY*Guit#R>)Cmau*rF%_NeabhxV=$ zpRX6ukPQn@{P0Co7Myp^Nf0GHhg`+r3Ek(b5o=N0M?9<1yPY9%6b~MKnS`2iu15-k zR&~TbcKQ}P1TYX@ogRIBKG4Z*lS}7F)w@uI(j?g1&Ubj%St=HCHbBNZsx4o2zxP&dO zP3uK}mTaKGN2Juf=BoHcbXWaRXI43AhF~xyk@=LRTk)Yg9>wn2BFdAa8p_r07D1~M z4u&IbO$Xe3B!i89e36*&EkBlk=YQ7wz2ZI*Jxn=;F{$@+p`lNd(OrYUYv{~b>Q`BM zsgU8U3iG)o$q#~Vw@BCMKG$4UAeDYU$!H6U5+Q+zow{bQ8M13?edVO1*24&~6V!@G zTL%Bn&Wy{aLL)DZ)&5n<=lgz{8p7^oIt_K41ib4V+Hk8uM$4L<(OVN!L^bP>7r%~9 zy`W7`BFNr87-M%w>>dts*F&s=qjlausTYmRmJI%0GS<#OHX19Otvm5R)oTl^rO`Jgp=RdNsLt?L$@ zzH3Ikfq>62(#1h*dV6c+pl8ko@se(1Hl>LNOK1IMIbPL+qEOnI%pS_da*3#f9Q~^0 z^rGpLzp>n55X*HUkvm&6g$J|zZTBW*{>VwIh7r$ce9G46l#fZOw$7@D#+^8^r?i>| z2>y`4lLLsu_I5cSrxlweb=&y;x2U@|8@XIRn0Mcn2SU3nUB$zcfZ|Q?(tW`T+BwlU zWmpB6iidizRLOs!?u2e)-AB5zlC>iq8SZorp;KHFu(%9w0$fJD$Bu0QxnpW0s?Cnj z47c_W4DvH>E=p5OlVA;k-CSen>&{pE`nzJO^|tVeb$?<7fuTrTgc zI|*=RcIF>ny)8MBVSH|@S%`o7XH!@lD_a@5ib z00lE%k)ey`2)ImIzW9;)sk>insJF|5clP6wDvqRIFyIXM*F%hFtUs3!?*ErqOXhET zKUqEIIw};H=#n`Mq%hpExVRYq?DG1W;l#4)bB#&;e}>#esMN$9!FH9Yn)UR_3Y zrQO>J#?n0|n10fFH=<9(>xCh);PsfgiYCh1g@xB}WgD|(TMaxr3<+Ql*V+bm0Ly4; zbu|E(?35qIa$~hv8$|R3)%z{~tGH3^Q1#O)l;Zm1%Phi&rQhEP>%A^oij_S)A0Wn( zQh~3r$UgZ0PE0TEoT!w`X@i+EqD!Aw`rXPmoPf)O^z2zD;KCk`R!HQeAn^n|s$)`! zr9$Q~>;L5c1a+n||4jS&GKQuFn;Pjk40SW2L$3msw-qK*T)hsbqXaelB*yC9TpA4Q_R+l12?7Y7=JDR zU>vO{U9f131;~W(mt`2&QGuJNle#e9$mlUwJVOx$-O>#vDz|~aXIlkulNB@!=WA@e zju8%a0Pv{Nf$zH4k)PX7r3ePJBvoJ?26@Q zb){!u7CX~`wx4X%JIetvEFF7oCXD#b3Cw{kGFUgsgSM}Bsc!wh@PXKxHBt!sdJ7-! zBNKR{qHm)_O6n^*K`+1nFaF>gF%Z7}0zVkYlf9SHY$D<$$Fz93?LypveK72gD;(%R z77Xy;B$z7Lvr;}>*!40nR=_@<%9S9q1j&e;z$!@$*!6#qi4tvS;OVx$Q1ZX~K>+A7 zL01SD7lz$y@W4h*kyRL|+SdX`)}3^>4@N_dImrL-b7oGDyLVx z?Hn|x)=*Rdd^o_-!Wi>~>q=}COWNRiMomUa1nmC$RB->_1>g4X7K2`}dkh*um}MpX zZ>*;(>^WZp0shjG#|j(cP!_bHOwUgmo;QKf~6tRLKH2Vj(SPz4e zn!TT>0A0iJ4^A-+Cc`10vMmR`?d3oC&Hul-e~0q_YrSh3VFd$GP9ljb!vb>usJ4Ka z@(H#rMHC0DxL-`_TOs7Yl8CT90UV{sAbY$F%O3mTfpysOMOlrA91?-jPap=i&j~P# zp)@7;5^V#peGmc%{;7B#3icF$pac1X)o)c`$s35ko-POAiV;pq*r0D>z96Z za~@!PxnTeN1^W8o0W{_|CRp}%^nPKp{Et~BI*Jc!?f&p+Yzs&>z0yGG>7;Jjm7H4aLZ=#R| z7LKw#ACNx;>rSk#h^rZ_GAL+L2593pa&ZAKY^MM#d4fYOTLj!B5DotS7*AaltTH=N zurh9bTfq{RBS4h!?3Qct8Th`h;2tg%BP%-CW(Yp*tbuy!6S%GiTOG0u+c1%uS3EC@B*~pKM>G%Ftx{> z-$fSS+P~L7PYh^o2-`#gYzO;i{p(4!{pxgTi-!9mh|LYg;Y?JZ(D*`eXbeE}c${Mxu!Zo#96o~zI(=f%Q~2xK;2ai} zv@crmYceT6qNuL3i>lU+vPM$xg-gzru81HYYV_4`#G8gc^}hM^{rLyZXWRTp0YT{9 zOBewdY(kG}ti~mWqAZOM?wgV32!s8*P*d)GrVj^v@E5;(ATrO?wjIUwKpqyn)Sy}~ z_4%E;07!w&a*mG4@H-JJ?|l-D6G1pB7s=6TPtSO=Y6Z|7l+S!MfBM>STuHCN5xD~q zO)AGc;?ViJuEO??51T~`|IBhK!0@%OUV|qxOee(N04R(hkf4btk?0>z8Tu>(t*^_H zJ&Vh%F+?8sUApbp&?p9+;-$4ealh+)NNKJW#TVw>6iD~@>v}@sXaV=eYi%HP~aV|H9g?nT9 zQB%BUuz}d);XtI}=#R3;<_Lo#0o`a!T$^kU9NQvMu;aVeY$6xT_Bn~RxK5{svKwWn zEU&2-D`Vg}X(&+35yG9?*Kf!Xsep$;z@DDJ3V07l!t9!=;6P8&TXF9#Hw5a{@ zOaU=sk}ELy?E-}xJJ1q~r_+EJF1dm;MDX;}RyEJIUa$PZ<%GN7wW+TMkZC~TZ)G$? z45lLIxWDjH7cnT`{@Z(P5K|%OKC;{mtOl^={ zy-4i~|3?-Bg;NG0tBaQ`hTpBb_3P}@W8671`5+8M6VO2Sn(gFRxNePX%O|JXGpJ>> zUc_8NMMi6h?ZQUK2x@e?KRkaI=oB*$ytcN^og#M7Nc^q;@TBU#9^l5yVkiI!0c3#{ zF*S`9lgX)3Hduo{Zg>X7eS0ee_}`bI35G3oN=x7W35 zb^lXjkxr!mY0TnnlmqPuxlhII`{`FYKQJ6=1ix)rVq#-2*6k1w&0 ze*l$Hh2%B>_#s+3YvX`J!L@po zvEKVhIP$>{qyY($iw)}dhKYDha$~i-x(r#lH_x_a9|IvQPHi1-^B_0=)`PYd=>(31 zzIT=BcdJZL{nkLHu%^JA5Dap9oknXDVHe=#<_VP%wL2zX&jfD z@~~M6m|bIg)@|`SybYj13-{&h=(n~Bty5V#T0OLA*GlVYRq+YBhkrMvFj_h-k!z%!J219DbTT{uTA*44rbFf#6`!gHH zc)DI5h*g49~9pXaPcgpFK2AypI zUKL^(V#Vm-TL0!VfTb&c*W)>*>G4K=$;IT zXwM;<(J3>8K;m(&GCElQ~u=Gz}&CvFV2%>#G-Oz6q)& zmLH-g%q!8}F~8iqSPZXsvi^mk&Zx;%Q(Zb!*A66AF@S>PtE*gOA)CX(K56Iu$(O0# zbEd-by-NTo%4=PS8_=4*Fb-pr>AA0OQ69OA_s`$nOXcqn`Nu*CG3V&?EykAYbjaHp~Y6 z$0hqOulvU(n}T`Uk)`f`z3qQoGAz*kbR<>%zrOIlT(UMNm_=?B6r!iIKcv?K9X55|*AM|r07U<{Zy9+_n!Q{#Pfv=b8@=IG8zk*L> zpVfsi=ADiGB$xj3iqb$KOnYWZgpsJK5eIZxc}FXX94gt+|$c^+Pc6rUs)Nr9HL zf}qaXO4r5LLR|^Hk$uZ>7q8o0}Sfade{gU;w#g`V0^JkwXN~3j$9} zKFO3_6MWw{3RGozz}>W1S@nD` z**bRo!57F87&LiCY@anQZ=cN~Vbi^+w2%o<4w%V)8<+8g&*}+Bo}yokoTSmfc>unE zJ+6@FVS9j-;%LEJUPo)y9pOE2m<4DM2Q6KNX(Hv`DbB<*CXD3qg12D{^V7oKwh6w3 zFNrm0Cjj;~lKde9zlj|eqn{;yt`GN*x4CvAgyvK7owLRNg#{pUua*|VnF3m$lu~|t zUxb@US9&4}sa_kUsJME2AkNLtdCquU-y#=sXm*A^>4>J#h8OTW#ZEpMZ%gT-jG+Qx zVCoDI-8tR;f&d03P0<&Kp>^;6I0Iv2Xm9tYU*T4}UMQ9(GRV`%Ts#@mEy2{g5kZ8Z z%IejRX1hO6hg}W2npK9( z%ju=NvphT)$YEL$h?9_s)CHT*PonCFp2rR}tRj&m-*0j5LCWw8j}1|Vpikg=Dc==i zV9eXE_4lm00P)?ENQ^jP@2h<>^=>~NxLZ>0q~u^OEuR9SiYFMhvy5knCe(Abzdz=9rHJTO{i@^>}}f!T2KlD{G7 z1aHJ>GlQ5;FN@AtGU`v+;fSGtR1uSmQl2Q+>77MZt1TIQbGcxQI&8^^mb~Whsi`{HPe6HCC5gCh0basO{n%}}8t#K&?KJU1o z$ZWM?s9LuIFQ!iGt?MhRD04Jj3hltf7#ek*xfTc1H~vq5Y~b*hJ8hajBUm8Sgc?eo z7O=nia46FE)vOxPNa$T#xsX<+Jv#s{2kT9rWJ!Hn2!0{du8HS=5Nh>IK`*bh2&Z_p zvv9TOA^Z=A(OMQ{#%A6QpWT*jgDLHEk0J@VWc8Q#I<4QG3F|me68fU5Hs2Q4`CoXwHQnbazZ*qc82Hp9GeytPn~ z#OHHz9^pLHbEU~E0=W~_Ciz-4Q4o}P!-#|-fbXsNf>5>FQS8eoxpKa&uKMTsW(b2j zT3WcO1q+2~@RD>I8_RPdp@>40#zK@5m&Q9?i>1C_o9};p_~O1LS9F;!V3}ioB?=ca zMtW}K3TXz4x99uwsEUe;0-l#je(P`G&oPmRc-SbUBEx~$Cj}7fla40yYbJLSLazS= z8>n;b6N@alvgi1(tuejBh&7Q7XI}k@4~ObC5-A80e05!poyk=B!tTOwPC6IX<)HEY zVUu(vY32I<&ZD%5_YwQ6m*K>mk&{Ao=dWDejzcxjS81ZU`#5RKy@qTDZ(N>BDiZkl z2?milZn16sZcd+MK@2=2Jd=YYuqzUA;WSpuS|7X-j91$jJpWevlpSw-%JNeymv*|4 z1%pAWcV6imqHr-O^smN4mA#pbj|6n85vwm;Yu_(occCPR)DH5FHkdf|ZH#%~oAMoXm*`q+G>;R%bN8 zW{HPjN-%mL{zTM{Y#&7s^1cm5L?>ASd79rfR(L9TvY#2$i}2mw5;bulE9tea-St+8 z(HZ7EgGYS+sk-nNeFgS(7%mr|B^NYb-MDUb&<_;}mkA%b*c6!dq$=y}nR|g&{;}qY z`_b_e>aZ3@XkTn@8>WK&`WN1h1P$l=xq=hTdg_uAmH|)Eip~h}sTpuJFbNsd-cL-* z6}NQ=jHy4X=71UFsa5XE0&KUO(g?i* zhNkmjS~BlrIw;&5ZBj3M4Lty4^|M=U&j$F$cy%v5xqRZ!(z`2NGkXAeH~Liq_1AD5 z7AGwrem@*DVAh)7G3?cD3GGhZgUyuWrj|(Z&b+3=W=bPX&{bKTym`Ic_UwAQ_QsIf zAxELg`TasU&f7Gh$Q2wEFKVdDk2DVRz-{TGn)z!-jWJq#CKb2UNhceHcPR0tI`B~> z!tH*gQtGo(QKS^cpQN|FQd^=9m~q}u*Q4oh9kmVVzK(7dp?PIB3Qsa)|1^Yl;%76W zv&lntv+MP4VJ-idWaoHMYs*yq{&Xwq1fiOwfr32hI%|>X*~T8BCDVf36I^-2aHf}4 z%Zp~HOFzHox%0?>ndZ;*z3ls1{}jQ@>(odZk|0}Lf>WfF?Jq=UkDW+XZnrF2|KXtC zWPJkj5BCriJ=9QzS6jE0&v;kK(PokO<(T=H?Jti4t{mEPcjK#+ycxb0TsrOCSD`p8 z*?pU@cKOQ@2R2GI;J>g~$mZTqKIW_4|Js%8dzL!sO$|L>d&pJ(r6!zIW}s0w`7k=! zJX!ivgjxAT;asI9?Y(}(qa4~uk)X-+Bahop@5l1tWZ%nS1)dTwvqBQK-*+UQDdmc= zj~D0>I^Yd==Dx%#*YZfR2sNdTdF2IfIvo9kqL~7kLldqfnl7@VThvdO32U`>g;WM5)!=3+h%QKN%sc z3{DoJma$21=YuJn)LN+KLpqs(D0TFAhc)F)#bjO?b-DHB67g1A$;QO=aWMt^XM~(u z$OUuzAEEPKlXN#*l29&jH9xd|n0@+;AX_laX|K{9smSSN6A;gkaE68T!n-;oPr{BD5HoFtNZ>Fl1Gg)})5|AQnScRBIT zIkE4Q(atw3CsD?ZdowsSmJ_0Y>A1HQ_$}7=0coz;0}s%5#L5f|4X`=_5KiCvt`uE7 zgLp=gW#K`*>}?1{KV62?K^%CDb;6I({1B&{mxoeCf?w1n+Wx*((IW(;Uywz%PxiG{ zF)#J&wfA%0n=t~hv&j@53o9*nY|W1R$j29t;hKYYde9Y*4n2K7g~j4y7~lTrFDA9W ztjbsU;AOvwpCQN-)f>W37Od-vC{Zgs$l6u&Cg3SPxAm7hL3ViL%Q%r@jZ!wFb_Ab{ zp&+>hkbAQ;+Kad|0;>1Y23-tx{XcuPGS16IXWuIh*EfI&n9d z@8o&E@xwn*q{&X^1-YvlqJ*O!zE4>3w{Nt7Sm6gm>3MBm(L^``8l`+XAI|YxJrZy5 z#xs7-jAcIX#;FR65jZK4k1GuilafR&jc5=G>d%e3hrW-E#xfRsT*C;X1Y(5TZA~N3 zlAz`?5Hw#$B0fi?I9kPSK$3f$_4>r=4TQ_^?T`RvXmN7Yz;`#S(YX2yH`fcW?I9#mr43G!57MdH2=&VQ2IdQl@ z&tIeI2&5gNspW77Gl8KON%SU5#BZEr+}?5bHwF-em+*ssqEv5ZB;a+84MfGIK;;J| zK!lmtkUDBfzXt;Fhajw5G=xbZ86NyLosZmptyinroeMza3$DXoki+ZLT8Aup-zY?E z0_|+ea8`pRrA?P_>$cD3> zM}oV-nQZa&em~*-;Hdb*o8VW-{N~Zd86wqN!V$&Iii#`bEg8MdG?KvF3Qh{k-;0Cj zLcSKR4_8jE3j)Cm^g2)MMuhi=Z6P>EyHjPP?&rH9;K*O@PhiAnH%54hMu718_3IM- zMqG;c%9EJhh2!;s3ET`o%+mA;8x468S|7TWK&K23V9ruN9%F(RnDlo>0*k*e;Swr> z*FsBxIU$8?0TioXQv7G|9x;<>gxv40Pyc-3vPz8Adl}_P%%sHz*!iS@k0A<3660UQ zWhJ_so0^oW%oV7cmfJszfjyPYd=UQ=3Jwa;QB~$>0L050Ytf?O$4{Rf#w-C1Q&gk& zPexr6k#ZR;mWTow1X&2S9}5wheD3oUFUo_HHHGC0;Z^huTTa@cVd#+Md$}gB1Dq`0 zcNPznFXja4FeT6;h>$5M9m}(xOXSc(h#^_bvKH@)LX8E8a(+_$BI|it zOPQj-!azYA$DmV92k3+QoZ*1XMeo<>b;JEOLm&hctT6(W24wX}1zfWj;_db7?cj*{ z9NU0wa6&y`hp6>s5m99G+OH{9_+XD*G)ai(31hrn>;b}k@weQdw87>q5baf6`3{8e zsb|KLh&Y4_%WFUUlI-dOtVD->olVzxi!uz@dsAlV2C2)K|0oACYL5Qm-02)jx?c z6x++SfWekd=E%dGu$^zn7X6H}eSQT+ff4 znU6Py!azld2eym@w~=&yJfM=Sb~7_TUda&KWINASgWXnFANnkch!+!>nQEJ^uCABC z#TQcx>Pwzi$5+9{;mSg-kkvpPl_jrWEvX0s^}bRyt@S9PC{ke!AHx>UWE@R?7M0Lq z;zL(U-xrq}I?enOaZOL->!JK!c;xLt5{cuG$DaiGuekiYXraqPzc%UODXz;1XMg*z zSW?nqz2XE?T&?Qw0H;Rw?Ru0NbD4f)zHO!)mve74Igj%$zWGQ-sSp-#!t9m(jh^jL z5E6#yygwd&iR!oK3>qZ`!ym^BVq)jji#6`pGUnRxKE&4)Y!0W3fxqh3+L&Z=Phb^{ z0OY734<3v5t=i&Z^@lhef3iv@xhJ;_P_RE};%JA^?wHnDPk$|{!~6(>=tW~paOhQlooi2O)piWgwx>oh(YR-p1qkg*ib1^K>`wy#p&xYFU);S+rQzlG}Y@M7@JM|hcg6TfoT^>;aBF(oo2SAy&q;CTj z&4fjzCmBFu$-;vw9)+D;fbh0G*^Kl7l&6Rh^l%1;^+nK;xDGf9LqQ=8M!3YFa&gJ^ zU{DtTQz}MX+k|ldBiB$nhXFlIhD%F)rVPvnE$uqS&3<Ts;f(&tY?#L8p)lHq4|)zs;hiTEY_(mlvD?INCvkE3r3NgO1EizmzvlqdEpH2FsEKDb4vQdC|# z|9d`iW!p-DzH4HCrw3wq_7z4S!bYiq1Dr{py2h zl8`uR|0zUPHKyCwG^}Z#kT58J3qI9_*V56mf$QX^I%I|mWh_G$#5gRg=2I~cZt)5x z`$2hwjM_D!ST8^IWyR7^c1P1Pi<#?Lm{;R@jzLueod%TW+Hp6)N)uXW7UPDxF7c~Y zVH~p^t(g1W%{aphv!4Or`C&DJSLp!hEPkhhwimN=b4lRzz z`W=qY1g4rB^VVnI=CN%$x8`axa-o**VbXsZ$VHFyk%(Tcv0^Ce`GjmGp_%@Ac`)SQ zJ^6dmxpjBc4-FV3Kd>QS%YXJXk{GEh%HaKMIkWx7z-N2)eug(T+I0!BQ4z>k0VEP|_}seICH)gCH3+dqyyH6_L$mYEP*6nRBI5>h4GiLc|XVwqdhyMCu!4kjmPbjYKGe~$mwQs<~2RxNPhhcQs@wf zLiSsUC?XJ1mqtcUX}+FXp3JWl$!fd2?URd}>JI)=gytU@ekEqAfj334j+&(s@sv)j zgekTQB~Xy|;|KM!-V{eqk(?AWw)dxwpQ2v;3A){%|Na?jLujMBJ&^QJ3vn;k~vTWW2H2LL<{YP-KHpXbo9I!ZUzD%8w&tQS#8?Xw=-IiJ6Q)I^md<*I3-Vl(~Sq`bH?%!k0Gg500o=X*D-mFLgtu?E>~U8C1b%aFo7`JJIuXT z^K#&3`CE8$D5A>&BP3xRuFD=1i?VS!_T^} z5=h2lz6$ojs#>;I4c!3*FbI&{8ZS~Gc#2?sEC`>Gk+B6z0vQ=5aeWx0qLPUfZyp=` z85C@_*nLDyJn49JTebhw@kJV2Hu?zb>cm;XrQiVrtvDi!4SnF!&&t*LRjkW!#ZlRU z?GcNs+0C61L$6Yyn^S`u;ZYZz*{GIo9BIb(rxLL<2MWy45160Cl2Im(){eyHzI%xk z=W>e|9;>rEYmo}n`=CF(uhU+SF3S|~{xsYixZ;7M`TDgB_Fx9lj!Wj7EjFVGUjABW zPqa{awI*@ksMb@-#cV9dkCSJ7b%kXNl}-dTlAnv5*sDL{ShhZ^ zjnDbA*cqpIq9ga`;|kk#V+A&UOT~bmZeI*l3t%$L_Tq?GugZ_WE9g@hg_3 z=8G!~G2)^*J)x04Pn|m&>kgEfO`YP4b)Fw#bx(Cx333z9f=1za%g5VzG4MjQ6&lbU59LyLt?ziU{?F0&8@f3F* zjwUjcEbOKE#I;bH@j(Qgw&Q2$-T{i^9PRJOOw{Q=I}^p4!PtL#>WX%e4o+3USsn?T zj6wx!BCp{3bLhZS?p^3kk4qE=6+Z7>Xq=CbkCO@WoLIO2g){U%?67B zxV)>I2^##H#=U}b-G^frG8=l|b5ctg-`mob`-hQrJe;z+i`HN$k)MA-4b(QOc3%Pks>WVtXqUqs6%_GezjLrJht1;;E~x?a$v3I8 zH>)vxHj+%*Y>&Sw5=l%6b*Pgz2g!>xglktohB!)e_(;XNhsSIu8nUH&Hi|4~i^N9C zW)$$JNnJiJisJ3vlBFy?|7ogO^j=}sQT;A2gcNt@BJIs7eg(0U%pKcvuw!&ChVFQk zs?5|m80xjyN!&a+9_Y|6ZK!USDvR10tqwRJ7?wal#~?cLj`}v4drmTmO8+Zgyp`py z;^U?ebxuVMViko%tP6&vNr1B{~pDrQu zx;~{-{`%=!$N49Ne#5Jko~Sa0XkeoD0Do(IUk&&x%KU~h;cA5{g*Ha((R&7<%2@ur zQ7|TT=#0MxHPL2nMZHBl}fbO zYk2*vrx+St`?`^VG>Q62{AFeLy42nMR*zV1eZqaS%c%RwM#N0{n_M3tveKI=ENmJO zaL|?Dvunzzo!lxwG+JRI#dQvRyh0l%Gs>saLvlPlp*V6w>b3b17?goC%JR>r3ygmF zKM`tu*V|+ohcG<^BY0YR==1g{+yrUWb^)p*tLb2+SJ@#(MPjM_=a4}|=O@Lwbms3y=E0U()^`6|$V_>oECMl)q{BY#Y1MlYfkUD(;7;t(oxoJ(viw%qbxH+NXCH0=C#+C$#8vn-0RjF(*S?>bsqAQleUY3Ega zb;J_{NBK}!yc}O4#%8mEbX%$}S3GH3QdGhU5*mrv>~@#!lnMd?R*IokvmsXM+OS|x%?4)Hf`_q z-D!6)0H9|ZB14+Zr*)VN>xr;7A|Dvu?b$o2J>mY)pDP@ivx(7T(9;?Rr|%9!~w@%Z5k!7$(k6qHG90l3=yJTOYn zwgG~l&A1Z@ID@jt1;JlW8OL-BRj8?Lf##0{aN5aqr)+(xXiOAT7xK@n0PjL$*n`-QWY=m(2kfQVJ@J`G>;+Y^6NP|-x|0jgE) z%(~F*5eDW{wsS{fZ#?tsuM$yyqP!}`?Ir83N4Lp}jfgto0x!LL3U|sp?W=r7oOACf zAB%o^me-<5VaVsW&5NyFxp{APziR_xM@d-@BW2nR`RAxVvBERhWd%>yZ|dFlXr5ZS z;7h)}sf2qIU!bhiq{qJbyjRmvRb4NSL%Y^mdu{N_%%c^EhB5qzW3U3=?h(+^Sq5eL zwtjt!Dxz?2l@u4}GH7y@^%XZaE7rJ@r&9f-!r%)W5oO)pQ2ah}OzMeg;4fbl$P;pRJ+N^|PUd0B7e7?#eY9>VkL19gop}kT| zM_k8LxhMwaVj2s8&_U+;O;xc>r%E^33C>X}w@pf{EQZ^a#FE7c1};Tzr4zLGE7Q*} zybd%#St!4o>L3chv(k#(1D_M?+#Qph7y>5s_2)N5-73o77yHD(!z-H!t`2QOb(zk% z0QwO{Uz^_lq`*;9Hl#WirG_l(n_1Ma9xHiieD;uKy)EYp)BlZK#{0xg0=eh zHV!Tk@dz1LF)DdQyk z)tb6;9$11;!}P^teI{`;BN<}mTZw{QgnW#f>!{!&K_KJ7@d zF@^-47?|c)aUR_9BE^`!e5R28JZ714PB;2Z0>jowB!~%T%O`VK>QQ*A{D`lR&Z`p( zdM+HnIPh4!Jx_(9odJ^Dz?`s1qmc3rD6DGL+hrHA4Ul{FVm>NV2|FFYMAn8U$1ycx zNn5kJS~mgTA$uQd$X#WuVB)$-iT=8L%SN%h?(+L4z9W-iFF_Wymc8mHTr@tCZh;&~B*{!ruku&R#+P~RtuwI!CP|rI6Hxbg^KE7XuSU1`54BI<4E~1v z^lE$G8Ge?RLbD01jT6n?!;i9ejwCRz^!IN(IMtA1wdGt6T2ZPA+3%%HV^qfs>gwOA znRhhT6Pu+orPG6pf|p=%EK`{Tbt;+3v`{M91T}G4hA7Nmjj;^=)W%eDDB_W(>3M1{ zdFgZ9c-dHR@!o4uYN~nY-|tw0JZbD{R;V#)jT!Dhd_hxPNFvylFxYApgxuMr%MmkqQpEbiW7_~2fid_( zXupmDFHmzh+)hHeVm2tsvhQ`*iBUVKw{JO&!v|7snqOI35rPq-i1{_rDF^s5Y2TI@ z-xxHIx3gd}w(3b6V#*EUOec!9fOKf(_P|e&%Ks_;as&h6mZ1z9uaR6f-KlQMDLcO9 z{LTI}+F&~Zo@=MBoIlYJUfQWn?aI=VIMFccE^F`bF$rBN`#Taf`!+i?ymnx6#nJ=N zOau!irMf_2d&uwL!O*Rc=n(uqX4Aq%Stku~Wh$vPRa-e&fGH$*t_AlG=5n0wB84+p z2)V%pB|os~!ER^kwAkWhNnGM-XdHGMS|XEa&!{_T*-vgyYZ8mt4T`3eb`z6}B2X9D z@8~7sNjqs8%Lp&5(0YZ8_LJk7V<@^^{*k)N;DLyiJ~=b!x!%KJSWIeJ$8AFTN(6j% zwTX8xJEB%(`L%Bvfuxx_B!QtaT+TpvowL*usyfPCd!M*{3?Qsr-mpvUp=o43!3mWz- zTfpLY|22G*4!*X(xEO4Ea*#Xk$;DQmZzTH!0_OQM(PU)Lr+q~xr`*d*(Sn7B^!^p1 z?SDz6UpY&Pqurc| zDQNVSa&Ef)J9N%lMJU`=E2n3spCgdYV*Fzj6;9iN0X@3!T9=VXIY>=Cuj(ifXC5=u##qy`^3)SR$qA4e7+^7_3jYGW28(p zaO-;v$ZFj9^B!6^c24-MzN5puBnN>b*5{NUuxW5cRf?@isrP1>I48MFy?3v*1|x{e zEdq<4es13TKDydLCYAE~2K^&sHHLP~YAl6Uqs>~*nvplOxHR|ouo;Mqv51XC-buIV z8ccj}wq{xCv9+-zQL@sLvS|)xkoZNZhSbwCGMz6|`O^it$^C@HL4#DXN&uNya_jbI z@yyNp`DnhIBnz%nJAt3hv5nWeLj*sE@O4d9Oh-*n%~6xbepUTh{17 z7r%*jTUoC(&4h6XK*sW{2_2!>ngKM!Upu_*%5!D{xl~eE%R+w*U?{CzAE)DO5I6aC z9_5g$f5k6pYB+?C29;7!UF`1r1=afT5wiolrVDy7z#7KDe%S#-s@-RIJHW?3>f#cC zRB`enYw7Df$?Vr6J>Y?&J4pejSUnbw5pte3<|;b7h^U)83=*id^NjPaf)qYJ{Y=Q^q1&)&ko)Gn< z$5@k*4IqF_O#qMpp?u?yz@7GkhHA+jmf_oM!=TKnCy>&U(7{V1j?luuqIff|HutFw z2ozObwXNUKLUxWM)Z{1CG>@+VoaIVx;tVYJ0ssDrrr7BP7otVrR#F2muE7`LBBDy%T%5QCbX zIvh&Cn$$3GiW72pF3wOIhR3QQhoDpa-e_3HUL&7c1i~d{2{N4+QxK^J3ZMS|l4`zX zDMf~|)VD(tIgZzE!=2`kGiXzWfEy-A$iRfmF{ov+WlOMFuo(lfwY?6P;D*z91HkaW zcoQYnI!B^xKhnSx6;|5Cro5i|?ESPlTZB-4KD{#dfzAGcgl}mj{|9^T6&2ODyo*Ydj6}&umW&`-vSg4< z3p6=NPLeZ75Q$2Xj3}w0$OBBf&36dp)63=Y+`Hj2(XWysuav$yIcc4Lfcb&quFPW#|9WVUFVAlQTOAHOzD znoXm+?K#2WI+Yr{5=SpaXWkJZlOm=!zD1kiCH>l+C6U`(==Rm|m?$Yk>W9VOBtJfK z@Tyfurv;-&5^$-Jf)oxoC9&1h5*!u)zUj<22i_%fxQZiQf3wJUJ$V@iMB|>DuI%~qPO)^ZNZj=%jCrah$E4WNhegX(ENAyz9XjXf z@&;#ub9rXW_F)o?`Da;1c(^~o^40s}7fUtG7IP)Ow(3s{^7J-RrAVik+24L}AMKy^ zdx;x{jvsc|$@*Y|9#B84`Xvt<<5=3nD|9epr(oS0BZzxOXcJU(?mzNBa#S-FyN1ye zCgh?xoRulmQVa*GQ+H3~ONrqrAQL$+7|lH0$n3Jc4={;)6C!nATRhOk@(yI_lP&N) z{VDOx1QC7=`IWETBSMe@fxX_Dv@{}O-A%K%9itjbEHa}@E{9?kX=Js(?(B8D-p1dZ zuSGMs9gG|f6u$J5w$IdLURE%*Cawv1zLc@74SQrR`Fs7}>xHJlw@F z^md9fnsQk=is0OY>DdI*Gk&A>_m-Z78f5$F=|Hn%(%L~%u~J(ddKnLV=h>2c^nrO2 zizjr7HLUsa-n91$)#8)Dcv zL~7=qRY1LqdQuPrPm;-NTg96fzwBZlhd^JcBH+av8k#V7mGs?UZTl}04PLB^L%Szb{;!q2WhvcE09 zTUU4`Zs6cgM4RdLovzmJHS<@J&Vd~!m0M#KJ*XmxLVgzfiA=s>S?$$ViUN|tNIN!RpU!vekdlu??q)EK2x6@`p- zF5gjD297V_0b>AOI!kwR#pk++nn-FgWIk9t7Rv8RtjQ5iYn4WqQ%C&a~0hue`d zDTED8$jv7P_WIpGNzxtgxFFko#qWBXC6;C!ExdQ|vMz1oA6Z?AepOBdDg$0VuRa z6WvYgbKIQp)%ch4H_0hP^Lqm%=TB%9|K{GjwCADvw6l)L^HOjpT#uKY9dMPJql_wg zhkOt07hn>k$fpSD7Ym*)o*+CfG2J(K!+wzbWK<7xeW>}7)XRAx1iK~`Hc}W%`=Qxu zGyE*I zIbU`EhmY$mkDsS~{_Mt}&YkN>b%o$P+V@}Tot}bR-a02Cfe#)P`Lm1r=5?ixM6dAg zqg;A9$zR*-ReEobcg0>)JDaqIU7r719tn^?7#+X`SlP;OIrXNLMX6U1Ce8F0G`UO9 z_A4^+0QfbC0u)%(`62u_SPAx*;%w*^T(^Illc}}{`+p^$*ysYG7mGtE;>%sRU6DgR zvo+mUSd=&psk~j&KLUv!YCc$-yjZAnLc_G1*jid)k&YtwbUpp<8*p%u_lUFHVu0ZY z+E5jhv5`O^0D%fa4MIkoLld9qliyIUnW5S`@5$v6=4w3%lE15X77UJ;+nGZ(wx4=yBX7|ac_iEC?N<4MCz;FU z{YcxxqojLNJ?jF0Smt^5n)GE$qOcAdyzWxgF+09ULAY;a5sM}*ILv;3m4nu6k;NLW z9Y`y2YN`$m|6uf`M@8WhKewz)P;L5vg5Qf=GU!BhY<8VU`(57;k`4ABgPH6^JtNS_ zCX5*Ki!LISyr}rhIFs3&lOIe^Pl%KZFd0@;Mq*NKsL}kr+6qrO`MhWJpT-m8|KVrr zsXX+kWx;)&Brb=#D;{{)+4%qd=tFe~X_PieN_A5;4@7*0<)H%+O@?c~g zG5%!Lr0}`*@l19k;*iARd%8@{XoKbrZzPZP3~4%^W;$ zuzz%uEr6l(vs`O~j9ZV$nqyLPLm9*KrtEcwkF$=caIwC2+$Y(IMxW!Kg8as|Z-1-M#_@P3Bu+@6$#6BT64YxU)4@N$KmT zYIToKBtI_1Dl@f~UIksQzW5mfYjl3>yAbHX%_AWi;cepbyCNwYJq!asbcyNX)3fOa zPnL3i?ig71*MI5A;s^BTJUK!1?5PNq(aFRW!?3{0Ha*t?rgkVi7ti?$2--Z zBL^ipI4&TzWcp_F*J2A1^(0rPZMnO+$6;Z`?3MA8B18AYjfxK(#v8*~<4^f`33B}N z&}#!y#6SeT>_V-p&gwIV3S)Ek>GY=}EJ}_O$ckZuyH>fe9x1PmayA>;baWD9f$|Q| z_D&hQr9EMU6NK;??R*2tLY@8g_8!{{9h$Yp?4mD8ivn^UrRN#fsOSYF++;FLRKXq4TyeQI`vY^x%`1y5c?~g zoo&?8+ZFZ29k|WDHg?;aw0-q^RzxSL$yFr`3kLt{6u z1%<8=J!ivOu2kTMRJ7j^=;ceN@>Cq3H};=93`R@kc=hm8STH}~TJ^|`T-xWn>m`^x z%aW>a-q=v1zK>SxSv}*vuH)c$Z2K&%AT1})4O9%I3RDnJ2(TrTSd(Zw#gR*}_LKLA z#raF5Ec1>%SDKhI(v>#^v85=J@E$EF+OlnyF(MT#blugAIM^Rwk!4mqa1A=Cr`;=` zbti>)C67}O{b&Ys>JH0c8|)xBsa=jiHj{i87Eil)-Wda%=b)EBu+7ge5Q%7-J$4)= z!iY(|DMi*wh}V6TUbJ8SRBoxB!Ya{tN}InYKtBqi(}bq@+hbZF6Z-cSv4ZYfZdDw8 zUZW5IecL-|LtvkUU`8mPk4SY>j43-oq-??m4TB(7WrDgGr=qE4puo*D;Oc}(`CX9~ zp}n&F^U0Hb?U0BXHjq_tw_(=7f3`?1`&l>q+S|MiPxk3=#k-tv*2}1#j_x~D>@F-ZipeEi$?e4w5vKo!x1 z;fj4SXDaxk=9bk8)YUZ^L(pFfGYE(wurGeNV^j^@ zsrlo9ci+shhUuG2owRl$$7e{yxe9|(-M&C>HEXVEod4L1bJjB>OWx#&D;xhk(rX8w;}cPwPtm_Bx3=V=y|h~~SL~b~nUE;!vmI$|--W*x#bxzO1RVj~Z9f$Z+T zSZmei&Y#`Vw=9*vmz8P$jOcox5QbGoCYZAlbho~|G9 z{tCne&|woAJiCC)?ksK_I_ptXHGTeG13XAu{Z zKvC9aZpRqG-TXC5OTQSbnX6Z#L;2~JCl0uv%RjE8fG<^p%5EwZc!f?|dtA*t9ELN& z(RMs2`UK%mpW*=N0oS-~{4_@H&{dl_`E*xv*Z**_@F8VS!VHX#B znGFwYwY;A}R`v2m`{dX3-JSCaIp8&nmf3i$^{0_^50EK-o&UsV^YaC>+S_{?kGHPB zxU$6NiqjD>M)u~Gc&zrNbYg+LB#jSL`p&ODSjX6JkRR_`-!2?KgcEdsHA4zi9xo86 z!1d170-$w_phi;;;UGbe9g9R!tz!^dfJjh5m;PY_y=2c--LdP%Z@ZUH6Zi)1HlsII zJ*M3^-@Eubc)LBk z=KhO?Izfw`c$5T8r>etmkri*pjbOx46JF&Gi|tj^{2=S{vI1*_3Au&Q?g5DBfwB-@ z(u%zkIHWGY3~nMTX6dB-!X)OrEJx0or^~OK9p|*U zvi?*UVt)G+eBZOb7^UDq<;yy+D#pi^M&~6%J##WQ+DU&3a_8s2QxJti$>jgI(iZ`) z`zRd_6Z!_S-}3Dg%VugWV9clcv-J6$kuo6R(Gk(%(`mj6%KSJCL@lXdi@b7M{}Ib=Y8?l2aZ9P#(JM>9*t!4UX8*9#>yH@T zwj>rcp>v30XMJDE!Yr>&$zu>2kLs8e8`Ba{SdG%Pj?k+o%$6tas=e zN161&J%(?d()XD)QfM|tf_z1GK)7n#94>XdAzO1cr0ZE{yn1%rSOjY;MWo0!Qx$(KmHd<_k{; zo`J|#Yse8Z_#JU22RNEU`8C`G73j)2;BT{oj4zNtno^FKsNsY|x$^EqbV9npAFnOD zCw?eVCjOlUIZ)n{3!N4qj)6&>p#Fk#zHyM&DnwwC+MUTEPbnU7ig}{EWP7RX=83Ay z_Ua=-3}suGfgLb-4$>jjWZS=D82jJi*}-<&sx>2p6y={{t09Y!&;GjZJWOB+O1~&C z@P7}V6=6_&f8;zWDvMoq&d-{RNz4V)xch>b>L_`3lB?g=o? zmPnx573luW9kaxnHY<4YZ;-Uj8YyJqV;J&hd}v@QD64_;k4x%>(C1jT&x}TsJz^k! zs@~lX3;)jgAAX0X)n%t9bkDXA3#kv5!L-=NzX@Hj&w3Km^sD7Fd0I|Zhj@i>4<#aA zk&5dD))gHCOLZ?#8K)hY6~GuwMQEtNPB=u~F|B4l4kQo9gDUl(;MGA1jVKdXpZlQt zr}173+LuCv*gaOcq!2V<*3e?T%SX;4@^%i$KmQ;7=o?w+2>nvW&X9z0+}HQvzAppr z%*)32VCrM!sV0(hV;Q zR5|_5m27%CJ{S&qXmig!!^+bqg|vni{;ipLCoK)+`(NTk&XPA}w4tvGB0(;cMgK2f zgY!^vg&iI=fCjE;n$Gy=aIK*yu6zhqSdv)7?bL|wAv|ccdy@H{COEKB%yq1v^nXXKPBlu4)KsL*(&Z^hI^kY?Xe2U5gPHj(YFE16; zNw8Jw6mo`yRAXQ>{QZB@k=LlrtU;BeZNZa?sbWQ0l7OdHPoItk9f|RIRMhA+E#(p9GH_>kYJ_ zL#bNBy=veZn|{^}SyV9~tP=*ZA~c!cwoV;g3e3yIV9Yc#+ahK&eDKL(ZN{ZLxMnC>eK-RBB?w-W*1U2R5dsT4`|5R^QX2~xlc#<7nQ|Ky_%zv9GXv_&1dS9# z=>@7M@aNO@i$8F{KLc@Kch>`}5$VPN=aTXK@@X3&A#3qxad%}@&J>dUX z#Q!ebe;4k*JMOqg8T{}WszVqk+_Gc_ z?oI3yx8zAA4Ux^Fr~@04t%nHSE(hZ7NzyT}M@L;+hVQ_Um*KlnmJRHs?WYJ#LUmvh zqJm5S5Yw?!N1jkWvunHndp;GZeA&kH93(6#T~| zW--8RN3y#?+)wMnbL_5o%DLACx4xU14K9o-YY(x0(hXW$^11hdNw!M(k4ZkLK%{b+ z5=X_;b#%YGPsI2XYw*@va$C4OVMfPx_3Id$sUFmJBr`6w6LST$j91EJ<;Far3t6(b zjYMZB&XdW;+?*@my};j&3JSk8U)g;9`^(i*v1T1=e6|SYh-FS+hB%C|GPw8VHW3gx z;@Lwg;w&QGl#D1O(8n<;Cgd3u*c4AxlE?3fM}mlQLEzMxNZ{FH$>59Fus^sTRHJyN z>piG332381_1E&=mjyj$_v<8roDB)kzsny~m!g``H-9VirB3G3|Nfe?K>`S~mpa@pwfSxOcs@Zb=lO-xY=2dW1{#Kb^&Xic7)19!^Vd^CN0rpQ5u%RVWN^N923K` zEV%h{nFM_~+~Sz{w67e5;q3Wbrgcw{6p)=?2$_OMuS8ro?{GlM%GP*I-ue`qd=kSv zrF~^y5Lsf-U_ZGnRkQ!S0GIJqLI~iPT^?Ok?8^}ruP<8&({q57_`K_Vrm)qjTAS#!2;8r`lm(W^ zF30-VZUTf^HC|=tIed^lL`{F>i}{vCITDZ~FV?}%QFf5gt!ze_ zicSM4(YEhIA$OgUR z6?V9ED5}ix@x^iklYfHQPJH=`*-~~d$LY`~5yubo`gMIkic%y2MNG^Dhs9oXU0;si zr3Q(R?G26NQk}7$y^&9?<|e;-=D-2YB1TC@H% zYz^D>^2o{RMnHgS$|V-?8grOC&A?dzJ7Y;HJYOzKp~AU&d`oa=>S;aKjApA>U{iKr@L@y z^M`5X=-f72$AN4(roYHG@j;TNAGLt?1S0 z42tzM2%oJFhOGBQT$giKq}+G03>#eHMmrS>^sf!4n>`yE^>T$A3|V5_{Y_uF&xBcX z3EaSA0QfxBGmPCy7R`gna)RyQkY%EvRQwq$xqz%7yAvM*xZZ3(xR! z)JHvwhRtVaWU#UW`mmzOkp2sO(Eteakn*8HJ~QAUE+|bNntiNuFZKm z6YHVP!imh}zJ9~!2A3QI=xd2FpJ=L`Pv=%a1$XID=2=4R3*82yqxG*vYa+_q%n08t zR%8Xv2kSSb>sU!l@{x*)gn}j|!~;p}wyXhwf6k+Ds2%7rtXAGp>Hh zQ2oXAOqS&%<7^$(c*o&&Zwhn8^JdxdsuYiicY{W$CYwLNDb9e~N{7~cIu$z|h7Oen z`-$Udcd8bBmk1KdNNDvn+@J;Y3*7@~)o^x+^XsK1UlR7WMQsN%ok?QW_T4XM%j~wv zgD)rdMn4DIjypl`EN;+TFJ_t zx%@3j&fXDFd&^~0dA{^02h0C1CYRvVf1G2L5NNi@<47-%)d71$VrF?x3Za&%qQpjo z06PWa+svoW!0H&vHubkrbgPRpY0Fl1!xx#{eT{wv;ttyo3r7vqR)86-vm7x2+r<(} z4gRdXE~sTiy6DHl2I5IzoY$`SbQ$2*=-R*#VvGNtf%K>c#clVVg5R)p2>QGL#u2d+ zFobeMR2+cSWGJwoRYwBuoQ*ZLoDYm6r3c^)_g#5dt_(cpp(c;XXTZqvWQ2xC6@qg; z^uIU#znR|O@=(*JKOy+NE~|q>MLx&F1Fs7#Hgk!vh`1iU?IlP+wXTqY81I}2!Dx9PE0il(|p@sJf=f|@E9>#<1A^)r<*Sq)O z3yPhs#Nz5E>~J2HO8PG#21P`X`A+%{SYylwj$YRIsP!>BD}ht@n4c;h|-51dQ4Cu6>B(@O(Ve)lL3<<{uKg|9~qj20WN;@y!IiqT}HlMv|#M0bs-r_IW zJ;DB*mM-~!2eb%~T9QHqlQEPRymC(qG59l_%cP*;=7}!f1L!ybRwEY0SzwqBgnasI z5rRrQIG#e{Z&G+TdKLg+w`t(9_U6c)!IryXzk4NWaRMs!G_CN6T?xPr@j&pGMt~=a z%MJkfr^g%7qhO9@k{&6wiGmvr|L89!XI-{;13W(X+xi?a9=6XuSdO%Bj#i0I?n}n8 z9NPj?TL0fgOG=I=t;F|7vK7LA(q3hi7ztP8yEF(<2zVytj(B!qa|1fIfisDFyfTc6 zuP!xWb6@Y|c$`=D9!ow^DLjEJ1&9ooPNQBi$2f#A*+wy&34li%DxOY6cVq&0mk2z1 zxD8>C&+dgYfxKdb-`10rF7*)X0?gmhJC!#J$W$jfZJ7(nreNU=-wt5I&p7i6c+Gph9uUZz*3i}*dz=gPtLJ~xY| zWuwE4&5F6lAZI>w6hR^0#EXi7^J@N2jM-E@KE-1j)}S|OcW4dAwN|d%chGQ1+5gGJ zy6X;lE*=aF8$qQ&nDVy6kC1oi#zlNFarUgz`07s%NDS;8TTc!FaR)PNO=4FpumEy= z{8-pm2_c-_dI}8Rrwf7A40HdeeD9Z(Dh4>9ZJ*Ia=&;4-EmeNSB4);7jiY>pn^W}2 zb^Ij@+`J>QH?gYb;IQC6=E;kM10u!?thG~ETB4LHF&?cjb+^iGT&RS&+1LeH`WHt| zM#a~W0q;pHr+O$!`nw0xx^>Kvknl&~!UJqjs?~^PIkw z+SJ35Wp>tdd%w*z@cK6Q>-k#n>{95b@~d*eWDdm0pB71-!yBpinDg6>AhO#zA*nxx zryHC-Q*JlH3j0;Q=eK)P+f5ZKS1yuL61|zP&*xh~#s1-M*AvfQkfkw;tA?S3o@0Cs z7yYHRHGdBvKUcjmD9UZ-NOdLmRhs!AB4o6yPQP0g;rHhQYq9ynZ)zFKCLIwR2`Bnu?iOwJc&CX`pZQ(tY0Z=s~DfxjVJ%I1N8`Ix;b8{?Ga@ zRJ!fOilH&gikw&Ht0JAeh-s_eYo(+D4lQoePXst*oG_)yawsW_;8yaP`^ok^F%grz z!n;Z1@hJnHo~|acxruO{yXSjPy~{Hi{VuWl(2O5h^t4yWU5;0Wr19pM-}Gnqy?ex< zpmBYlUHg;X2I2OE7WT_Mh4RCbLngt|ptRRx-9F4_Qefj;<57m9w=kNayU_|F*m9)&V*o!!>D(`e z+PUMJG-sLOZ|38B7q%L?#YR_4F%G%#@(FDe7cFjx2&bXv=BLv9>cYC0p@SmD5?#!_ zYgOTH>npyV;!?J=2ns$6j_URPDDeiD2f;s?Mf|p~siwHzIEo%~Xi0b+QE;U7mzS20 ze&~HVBWqCUR>8*jG&a`ycDSe2qwsfhsnX8+n~iS{-$bHJ1DE_^ZS8LeQ`#g0ZpeSg zHWoCq!f!+oJrh#nJ_N`FgsTLQY79Zc7D*2w>N@>)V?0}a$^THZ_OagyeH_Cq8pKGr zOn--Wl}#^0a-A5U3L@fstwX-r<7e zgq2!+vF)|N`?Wyi{OcFi$5X8mKeTdpBl&k48a^*}+@yjoqCb8e18LSEbuMIiRvW_r zr_%MziUnRxP!)JCqf{b6!>YK~iVKUXdHRYr@M_od`Q@-w8;`o#AJDZ?$M(tJaBdtB zTcKx;@~A>2u62mnCz`=R$)7N8dlv&kHeQ|u-?=4zx@!#kz)>t=Xym{ts5&%Rs%O97 zOvNeaP?4uk1XGElA|q3Y=C;|*ka?YW5FF%v`_+Nv=*ft0kLbub6jGt@F)Sf@uDrDG zpL?10<+hUkP%CIa5}Qn~yv*GAv3IFmkJl@l!8aQkohD;bxQnNO&;RsG-_kklor!Yc zzF+jCw1w7d5VAsSbTYes~~`21X(Y24`wqE35Wno7ZF)>$egjSU%ok3Sm2 zHBaixIt@IvM*Gt!?Wow`rT*>Hh~`?~``*$E%f@tZ+nHA>5!xZBq~D%~wCj;5kjsYN zVpB*Ur~=C

dKOyYv_j^K4NtArc96bH9>N@!-!yx!jyd!yU3pd>AnvN~3(iU%o8nPP^V)*JqcxE{%&U@yhVOJ%+NOUNq%g^$)NJD0FZ9Kp9?a*Z$W9eGnAUizIW-g< zk$2!bfjG=L3oP)R9A1IC*uVX*O+0+(&+$eTAjZoQ^TcUzS?W377%`5HGOqg+va6BH zV=M+Z1nz!PNs$G`x{e0f3;i~(b<}mgI4oQz780x4fw;WG?xKRGaT$aG_JI$8z8BD% z@_sMUAOswTY&)o#1(49bKBf6aPh3Fk8q(S-g+ux<#AYaCt4@4$&&GA)`rd3A8XjDz zz#2K&8>}u0D4)>C1)QqU)DE4+U+Fw*7vlw5ND`s38|q_+OKI zpx~M!SPuAwd)h+LM91v2xhv|WrEN@_o0~`B#^MWbU(b|fvHgVQO(IF})%=SMLIKu< z8}CyUA4YY4Z`@y~t0o=G_VZQNBVfEstR^C;b-i9HmTuYeQAutyZc-n0Gr&>_AT*Py zFO4d$_*g9wr@^LvJJ&}15Wn#oX6pUDDRdx^s+wSzR6bjdTNd*L|Dvpz{-osi|o!jp+9dVjBPVFm#C4joZSb`D@`T{DVc$BPKyxqa~2(<+;1h>QHnC zPkumQ&oHdTZB-^@_*T z>OfAC+Nj&>fPI}V;`V_=FiG7_J`}W2wVW(7Xxwe&#F%OrYILtpY1d0@ML0eNTNf9I zlQG`A9l^KldQz4*M`fJ0Mwax#!-(yT78_NtOE*Z(Ts?=~-2Ui4LTvAlQ&g_y+4I>N zQ9@5WH>&HvS}%f^KNYgz`EG&<3B-S?>qeD! z?kUR`e7ltO=*FF|IrFh_6Zm?Tw<_<`{lZQZcHq$EPNw`SK|EV__unK5Xt6%&o$^fh zJt#}<@H`bB?+D(*bD!aA;zv;Z zOf^>oOM4f)OB9g}#(wB{FPIQ~s;2q*ef5-jXnt-_<>TCveM zUV#c#VY;qm%m~Nt9GH;I%4$s<&9(@1E*yAo0)20#dFO*Kj$~ZV?|Eztci&uJ64B8S zV8?@UO8KvES*im5xT9lYG6JHHXnLs@i@l{*DeaF+p@3rJgWu^k7CwIWP^Pd}gR`+N zOkX{lM8}smVIZ1_DN)?p`LX@D()CWA!_>;Re7P6jKPsOL9sphN$Yt>fIy!o>2Pmx^ zEjJS8dR}=)`7N^yVDxYWbP?k)2qB=$Q~*SyND^bf)2f)#-sy%4=6pS(kVkz$l;FJ} z;Ir!gzEC-#Y6CXk*1tBNnU`zIvZ&w53nkZ+qoa-p-21wl$A^b90HCe18)GdqZ8h2R zV{Z3cX!Nwd9PWBe0&7)O0?AqxIPj(5TM7FqcpRV_$pb7h+ZjM#bUj}jFiuGn16*WW zP=w~S!;DD2(_Fn0ATD|7YFuYe78V{pT45?hmX*e1Tsmt6cuNyHhb=ni+TSl8i9Nl& zdjUuM;d+{B=>zT`4S+43!xaz(;Nw$0)?5dyp3~_$-My@B%l(&~T0qUkzqM^9&AWrK z$Ze+Yn!?8*fTVV9E7mGVlnM?a(~ZKT42#64W(4f7K3BgV{T#`qr-fQa3UaI=xeK@- zJ+HEez=6Xc74ZZqfSFPOaLObJI!saw_&1BIrgCZR7+4oVI!Xp-Tdv$w(f0sleK8+b z)Zov^hzJJH?QsfmFD{_z^HTAZMB}K16`}}eD?K)z`S|;{8pYdA>w-hEu!-vlIhfW( zI^<(!LE6fesZRAmLHDInSB~JXyH-j{(KsLZam9kJd}t&CD3G7ov+N11hYnm2+x% zdAq-0q(vtLb(qIY%QfX0m~*9OE0&rpVsw_;{(|2hZ)eZr5$gte6N1;!A zkEH3ARHKt!e{U7#mI@XTE3xpUvg=?9JI_Z~J;5$F4Zao>a#^IgerP(&aN6H!Bx;i5 zmf~)Gy$!zkG>NboNW}+~U|M@YXS?qWO*wxUq~2AcNV~(i<57^xD{{F^`8jZy3VNU0 z@Z+zBik1tG3%mB6)q6L6CvR5@iuP%8yqmK;w^l^e4k_d5i@5!44JLD3Z+6)O=037^ z0FwLaQ}M(0sL$oxM{zq9CfM!83=)r=Za9Vp;#ETzW`*S&S~`;?q9A2O0*D^@cN@r4 z$~9IxD;&l$^p0{0*-Z^_hq*v`+Vz5RSkRS1hSfaW} zF?*$29C+~cpS!H_l(8w)x7B}MTn=YNciufWKgwDcg2B2WdXHSY(AO>=whnDamoLBB z7|9Dh7kXard_No}rx22Yv?D7`fXw*GrJA9k*y6*FJ`4NWKM2-He1Nj{&Y|M&bMCO6 z8e0vmFBXrWtzMMtoKtNd4OF!rDyry~Yu*3i?bu$HJ5`c9lqpF^R9FOv5a$)fQujc7 zPU>_&p5|ii#J7>Hkd5{{kxxeUx0-SEIlpy#&nf34hkgn~x3?VX*zAFflv+o5rZAto zG{iILGmhRPzZGaK<>K8N@rI5eZb=-{8+0_qXVTSc>}oaDf{BdQ_UOO&rN0aZR(hX% zSq*34LwJnahJxX%H7bJl?EJnH1#E-fG0m}$-fd*UZz(R%Z(d^;+%^AmAyu%SO6)7} zVN}@3ZS{hTX1?KWwm*J*RbF^0*B|dU5EqE&aE=(k&UGrI7NfUSppH@CNzsbep0Y>ygkPM_>1u9r8Iv4;k24j@WV{i(1#)f3klM%F|mxL9wnR;s_6Y*C!WZ2 z?O&|Rc%O9`YGGKNh`TN|92&AZrOs>4+EE#_RIthRX+l))(o1_ob!lzsw%!ZlEcZ(B z)Zx#Ene;C&!*N!*sdOJj7@PV8C6ag){z*sOqx8{8A6ninHszUAMFA)H|2{uT_RyIT zL1#0z^{{1HmI1?^SW1DG(=woDR{)!}y_45CL$1NQ3cWtu*m^v8rWbwpWzH?ew&Ja1 z7hFn6hAuQ0U-C7+Ln>_fXM-bPQjj#y*vl#!ey8cjzV;+CA-fQDkK zCm4R^*{s4^MEI>VP%z#I~PPptXXK-xf-H=Fl4htVo2=9X!+jz51bnDrkmXGgN`UeISSXwfN$7auU?3+ zWux9fJf`NPb~oH1_VE=8(jtih9*B5@@5ue zzkQ+VCP7-SmWwPJ2YyszlAU3>5qezM`^VY7zOL8XbhV-Y0%%TG8Zf6A7T@V1j4M~X zt%N1Q2qisf;_Or9m*0O$m$H;S5gcQ^%A-3}0+8p~{>-PBVbmMt4&Tj{Ga{e7^GF_Hh0Ax`y1={c- zj-(`zmcz@3;7Gm(3`nk9-&tzyPnT9@ZN;pdJ_H=8EZ)?26OodBY{3-=cI+vw(TuCi zq@baH-&xIPj}84?tW^{+Un%$^R!*=00#5tL)6>K69Eim){?u{y*WOZ&Gaf4`;6SrF zy=cK0&Jv64qEj9>)Wps_vY7Z0j`C<2(O+;>A{8&)@$~gQDU*Pgo3r$Kt;Og4u{&c$ zKkSu5%c|n|$}PDyizy;wGh&lVzCWjduCH^_@84__H7A=swxuGgwpMO;aR_eDAbX?c zpdG9exHn(OAX%=dKD4Vs9dwo7YVY!7jBPLDu}g)G&-$@(vzUOx2=YT5HA5DbW~8*HDCevS9JZpuQVnUIZ`b?dhuXCbZ z``4~-RF-|;=tiy;4JXnV)wuKZ?xppYsq>{^`kl%0aCDv-GSzyolldb~9M0$@*}4wc(EJoG$7E!;qsPHDA&U z$w1JD)A~4UnqVec`lgrb@nhYT^HD|m5^k$cPiA7x06xFH0r)Is0m0V z`n$7ksy5Rfp?@%)*85W?^sA3#{OsNhcBq(s%mM9~^3^kiO5ax(q@Qd7pa4)$-isk+ z|1g`RWYeU!mnhWNg9T{q3O^j{ORjN@{tXFP82r4KIaFaaLY#4lZ=ddGHfmgp8=FNYJ3 z#~bW7pXcECc>kK00X&@JzFGpANJb^kO$D!%T(4a5iLGc^I-6D)%j zJ^(+Q{$a!}Ag*LF>lr8f)08yMcFheKy=L`P!#)qjsys;h#;!_i6&?XN?x7I|*l^>< zmojtpPI{V7)0O#0iIj$0QRu;Y4XwXz;cQplkvS-@UQx;6Wlg+M$S8TwCbs zhqa~+ps@C*E3J`2F^B`_*D(Ygev&yM&%P=vC7)&kDpB2FIA{O>SOY(zmOO!{C34o3 z6DwKI>CNEir&4@W&(y~d0UsB8QctrDYHYfJS6Gg^4LE*u4Tz;#toj*bs(cPtMg`ux zP>cDF8KG7?3TEJfv0P_e}N&gJ<&Bwx>l&)Vq);L@Ea2>7!Bc%5NhJkKko zCHO%jg9w}eemN>nrF*Fl9_G@=Y$ftt0OfBvTWhyX`hH=d$}$QkXDQ$qpGwgFBi)3m zYpT?MpSG_Wy^fR>t0-8!ZK&wlqn2?7f!b;)BeEgET($)t@V7kU(RV>Fg}fJ|AEp7% zC(h83Yy!=9VM0Q}x{tCp_Md`}!qRFLFW3g~0|En=1Z{W@Juo3@6gEY>%;7K{0$gf& zVDw0113!7a>I*uLZx@e)(E5;{?=Ei?x}NU|H_9`U*tcdJHSdoYynSEt?8pLE!ZN26 zS56dN)ZD?Yn}bfmQoQ#=$-R3i9D3RUaL}tvyXLcY{VT&isy0k4%&HH90&Uzu3zY4G z1JDXYb6q{1Plj9Wp;3LAAI<*8M$zPeib(SP!L_QUveCE_dZfq7Fg`bDRQ@C7(>;Dw zj_X9@Y%A>0TABV-59@_8E}6uJ;mgyVBH5OJbdyw#zDMqBN)yi5C)JOo`vLb)dg_h| z!B==taHFg%?Pm@wbq7*8Z9*P=x75q)f8b{7=jW&fYS+rikBR2rHHxcx&k!KBZX*dF zC!*id?fs|Z-isscw@--o=RenM*A9}a(MknDv)O8rh&9(oo1Xj}0q7}cvQrxN zfu`C-H!KTXMAS;b`{&^MzPBuIGG>xJSYGtwRSXWQV5KOYS1X(V6&lQhP?=6LqpVAo zNoDHO#x%&~sA>MrV>cE>A5@&m?YP)vF!T*<=l6Q4t{elnYgE2}R8m#@q*NcKU(424j+6lz>j92!Vpi~WL?fjIOQWc!21B* zxlqF(g=_OGgEw&~66|`sH?PWX0H|?#ltL7r!U-v@!0P*s zMR!Z$8nMjLoN~~}vPty}iY@=#2i*mrc|Hg>>BX`V%{on+%n1E0s&H61X8HthF(Zk< zpj3rZyU}B~2o+b)i%=g$wA;WVywyoM9ox7U(5L$gRlb*$P^V{uIKb)(N5&xnLQZET z0Du{rg+8Sfq^Jvcu{jG96vQi2@ZbbpwiCdwVgNuV{-fBQ9rSRRWY1#`1gmP+J+$rn zj{L{D_vhXj>$15Umc-ZH~DNko!iBc9&qy9c5A6>wcm&>f++!izzrQ==@}UeocfhIxufRCKOo*d?>7fQyd{{WB>H)cDfxCywsHeb4k1VPW2BGIa7?cRPafMj3o ziw&&@0bIvjZkPbzI#jq#f(R8_fGp{&Vs1}WjJ$`$jT+2nqVkVz19g&$1p*IxQ%@BL z;QW02J5=`qI42*KUMmX?*SE+=x9d%{&N@E9c({y?`Un$ZmpRx3tSC~z#w)+re%IBI85ti zr#lEtT|99#c&-Agp&fu@xowZL&$k3pgU(Rix<&>S?dX6bJ^`Rw3>rC49_p1QJ5SZN zNOCC-VeL0M-M_#v&zOOPY(I6f)O2J(-p{+D}iB-?m(mugC6u ziSxk(PDto9GMEtzqH8D+YZ#@5onuldqv0@&>ij=c zKljCn=z+^6Rd1f2|Fv3BaE;LMKiA}bXDPdKF~x3?yMxZ>f})cV#Q*v!PR$qouc3~D z$j#Fbc8o3T7r0zvaj|6$xxX0~3|rQD#e`!JY;ik%sbtP+;j2AZ8vz(CeWR#BMKK8X z4xQ4QOs7E$;NQ1Hy7>ysgg%Oz`gZn=cw0 znCx9;aUXk_%ZNJ4m>UZq?aJ~vt#l`s?iqQhrGb=R#&jZg2tq9-gKc|npMQy{rppq| z{EhTlfh%RMMUO#A`FY1C+Z?ngbGR%|oWjwo_XC}nbdd|h5INN5!bZq9seNH}lAgpo+0`GLW2;y82|v}< zUH*L<}nn2qezDW9bS`BX1v_c>wRE2+LY z?>%0Gz&Vbwdnai9vgijUzD2xQX&d*qQ+HooHy7S@IH02L#GBOl%n!sODe!VK#>Qup zgP5NM&TD>>$P;^CJo^$9VdLOTE03>>?>brsg2EzwUTll*c}who{5$hE-Yx78jV7~M z5|u*bd-Q^8QR=s-xtW|la#qy=MszIQY_08<=Y?UXCK;ro? z)#0bC#WplqOKigXEt0i@1eGm605hVRBEzq2BZRM|ETTh$tipEK!vxOX_sQnL%wUF+ ztc|S3yyQvhK3|qy+3v2U5w`c;{Yf25c?GXagae(4o2_5@xcYD(C!8ln0dpvEywZt) z-Vr|yv)dVH2|ARDvgyy=bu(QRJcr_sPVE%1*H23{zM48%m@~z3D^hr}#TRcGYY46Y z-t(!OFZv?4Sj)|m`@DW=_AJawXO8AG7e5%S8Q9<)_vj?;Jz)#HS%yxp%9?Ra&M?la zi+cg-72^k@Y?eg}+eJny3wxH-Wrdc$HaEN6e~C!yWyBy8}N~ zuJuVD#%ufaZUi?L6oaGMCEb#l84$ffL50JI^3UI{Nl}P%3iS_eAC;$QytAn?0f8n!FhAV?pQ@OgWl#pwbJzBtP6Bgb#n6F zkA$-0>TShb1v;;H%^L`0!||4j9Itp2cHIuI*7sJ^k5&_7caGMJ_K4Vz0(%D)Z|l65 ztY2Mz*gq4utN+4v`@;h^R8<%l;js7IFA(dq3D##QGGC6Gx=YqJv@k(mS&``MM|lAH zoTzMa5H)5PJlDNPl%?l&hoFblZs3Z#ZthAWjie*p>vM`hv!^TjUZB_P!^B&d1riDR zt;JI1-LJ<6&0Y2?U*1cV3U7++{%F$A9K-zmGaLh=EvPLwLso?C&={#iTI zO1i85qo9@`%k`+Qb?@iKTrd7NR)0pSq->3b5B``bScU;%LPGzSZdxT| ziG=*Y>mqtm04**^7iA(_Ye%q_I)((qLjwVZotGY3UaAo1kMq_#*HIxlWAOSvRX~Uu zf&IVRq5%hlQr&PKA%8@2KKt<*Zt+i^fyT<+*#Aly29x7aoW{0Ofkh_N zamU!PN6!<2tYP$BK_m2H5i+e^^RE!qMA2&5mxjS^X&ETw2Jr?nURbV`7Ze}a!Nt_o zSodZn{kWD%5D?Q34e0)8%(zgKgk3=}!Cix)Gv$oA4tK(2L3Lff)mb<$qJm;{QK^=|~$_K8Y?LIs+v4c+<+8G5DbFPG4nT^gi>2WN%m zP8ymf$pe}YSewxufIZ;Xi@Giv&snZbyV@$gYdN=Q zY+FdkMVtg6=sG-_KYpZsVX${;E$Zt0BIYtA6;j_e<$Q_z?Db$ueC8CT&(oBniP4R^ z5-%>AS^JHG8-OFHl>c))N69V;m5dUv`Uf-;CvI^oDxW7Sp_~ZL&2_%MIC%4C|D_U( zTP@g#vkP<2ArH&c$(r}>1KUcbUObWKMc3h->JcC(lEOj)gkobEWXym zkIpOXX8Gm9Xp{-ll@#FL?(; z{DMTd+|muYahQI%3?E-hkGQ}cnKWU@q?vBW;2=wYZynm4#Me3qW_1I6Ndw`TzIT%? z1|l#K`h2$OQ0`Bw0p3-CZ@O6&@Pw1ywe@)T&aczpYS4lX6d{e| zS%P2bpEyl_1D-t&ZbW=iwA@M$3w)_sskWMBy{(BG&caZK{P_jRO0~+H=KI1^rdDGc zLddaNhp+3L8dMXN-;hVy9W2L8{>Zi3D{uS)eLacT39pxfvSP$EA|*^xE+FXNzB`%T zZ0n@*7*El&K657{Z;SK}CEB#(XWc9Bsp}K3XzCO?8>TXj?k@G#p2$A(BIPR?+6jNv zrv{V)44uHYkbK!7s!R5^l*5Wz5AF^J9SA#(6K&!$G(?wg*KHiDr^*?|PY=->@6n-nVsFx}#eJO0TX!7{Uino|E*w@P6fIX=TL1 z4@t>)8rp8)tn+DHE_wqS(*apN4wyX|nAjwuxeH;Fu#3>5jmq}6QCN)!2KgpnbZFr1 z8fX+YKRv@m3zUd7XZou1=R)BK^Gc_KOG{%!@@41) zOxPt9nm6#L3%JP16VDJUIm(GRxqjf}zN1Gcxf14(W@pvD>5_f=(mB)ag*F&k<$Zt2JD!5o4Rs;_|1bA!iN!Zusvn8aowr0USy91iriTSL za)OdRof`g8`SR_-a5Kij>tR0_4;n>yVeQH0QDGnGzSAds-w65u7fA4=9en#oY_J(c z#^00O_)Q_?dx-&~WYa+h^V$du3!SC-ApLn*;10erwqMS{1~6NbiwKb)!1y}wYgnZk zAaH^2_NL8wzl?)+9X2ow-0%!Wf#=;Oagisybo|y3F22@zMjAl?eE(%G!P6MXw;UqY zyIRB&Ov+2(04y?BjcuZ&;gJdPYi~rt?vaR7X9Cl?4CsZvcGbLiSL7e9*9WVkPh@Wj znFhkce2?Wl%^K6b?-wKmM9&TF`veXlCF) zpQzScg=Z!ncc%UI1;o*h^Y(ywxH#WFgk19LlmBV#fBNiyrpEujmJ3j5LF3yG!s0- z!qE_RRTg=$FQW{X!KAK@B4+4r6_~#rlL}vKJAmMU{GqMUP5%@#r=w)!Q;tfE^pFCDlB;ytMAy1;dY>W-kxzu9&Dq2R7Uhkgv<3 zpICWSC+x6IaWj-R>BQzi=`zU+-bnXn{q1@QCL8bPOw~*_fBOjKb0t9#p|E*1cYBxfeX=Qu*<-UDPNKu=a59MZr4Xy&UU)qeHO6Xlg_mAtl_ z`#KC#&J2p4I^>`>F8cXXG&bQ*rzeMc*$MY^+brMhpAx-=oJh*6JpLF=j9=>hwfhTF zxy;J+0;q9bx1|NM(M%C{`JKBHDyLD)$s`vF3=SYH@A?X)y16Y>0hu162l z^Y|zeD&(|_?zKddw$llyE8?HMu9>69D7#B4^JJY?gFf-%L+ZBrk2*c7l@<<^iSvT# zF9a_Rr*;jy-L=`zTP6M897<59K+*^aA}e)W%$M5SXpmEcMn_yT?30$%*yPiuPBByN zT*ot>ZS9vTnwamuV^lXs!f|M4_h@dE(16)Bern*NTA?P2JF1+FdFt@E)AC6D`TJ6} z&t%U>nS{No?R!$~%>5p~XC^ETB#q@EK9-`dCGg2a?zfK(N)#CFCeCtbd*IdI!* zY_&wg5C%2(oMUrY`~H-gm+5=iQ`1w&ztKNs^jfOf8mVrK)jO2)pM!&Uys(gro6`-q zq!VoD410!@In>jYGUXJ*cx!o%^evK$EOWBcCCF`u9W7o2zi@x~CFy3u&2~-;NB+4I zw=JG0By>;glUi97Mj1*R59w~YYhp7FJA57^IMVbf)Qf$96!>`Wpd7|cD}RzUjmHKm zo7@cN4s^^KX5Ci0B=}(T=^p9QBovkjiI~cwFFK<elu{tu5pp#*;!ZaP|&OD_)UTAXgzJ$S@mXBTn z3d`)zZDo`tUmK=MUD3f4sN*as)RAoSeG{N^@B7RvXQ}x;iz4kHF0CAvbx)49AMMLm zgvk6ddQRZcLjZIJ*a(z@Uw!hNO3fDxXYlk-iWa{W(4LW`X4-^KPSI*r9Gkn6>Bl?o zo@R1EY-=I&({Oxc00!#102y1+^1j=;+FD&kv;R}eke1FlmVh?x7S?&qJ5o$io|z_| zFDQ6?mxtT~Zc=il*+!M$eSj8m!b9>cs5h!rTTgvSQVuXUa7){4;m%`#;?0desXOyN zI}t(xicj*z$TLt*$D&l`>zhl?(`%pW)v5d1W~GFVWNIhy>^2rq+}ah##E!nD?Q%~w z=RZ>jW-|P_+fKu0f1JF8y(R=p_Cwv$^Lj1~me9D7bPq1vMdI76E(bRAeRO)Ino)^k z`Rku_fQT@qB`op-(2pd##mK*~?u+(Lszr%{Xs`K&4zJhl+(xJ2McuON66DqPQ$%Z1 z?EyC=->6>ll9~FxSCHfSd8}CB&WQyvGHcPITB2&o9PZN`tLB6bOTz{ouf2E;Z(kk% zf~VN$GF)V%&gAGg&qR^|9nKsBdAe|yV998#Xe#(1~AsQ1V0jo_r2&-Z8P1;GCaa?g$=n~gvF)%HD2 zNLP-fK^NK?L*Urkk`QFO-@cS7ukg7Q1eby*qufnO^oA+wGv97_;s!PI7H6(uig%pE3%?%4U$2UX&EFQGMZj}@If{dEl8XL z95Eh(`rh%9<$$J;_azNYTMC9Uh$3XLAB1=!`GkUL!%>(yYBFr0;IkY$T|MWl4dbT? zaTy3@jK3Q7@|s7k0x4!A?ktEn!bn8l1498*x26dtW2T!xW2zfH5rk_z?J(QmNAoQ% zL|GSfF26HfY0xlbF*upwOP5Qqal#nn!jq@g`HXqGJN^X51!qC@pscOK0qEBj0IMHq zu0dHNrUf(ia*GoZVS|W<<$y2D2V21vTy=UQ8eztsJ!qUshIu=gnyU;0#|3Z}0u^Fv zV+aEmJ>zI>>(EepHxgMN?_lk?&~^wfFM55sj$o$0iZGd1J5cV~H-Vhk zzf7!Z4JXzJ!GBE^M3Gf|&4V5SzKV1y;Db&jk#?au@3)cDV&Bi{mOhf?2|!B+AF};t z8$QB#T7$12e$6$c5qG{sM8*9SB+vJb4iAo#k_O;5LY~?O75Yi2zMjTYOLD-aGIl>?EfT9&SW!!1Zv`&Mv@O+NKtfgUu4hZ{qM zmUc5;tZ)eXSyz*7NG5(KWW$7wjom8}6BBc3@x&86Vk#EM7&PliCIP*kALJn#f|`}% zcH;3Ot3h8#aZBZM7QO}JDpgkQplZ=_W2Zx z31Bu&_ldpY$Z`1XP%P$&lv?$$U*-Anp69VfqwA|*=c{E8ZYdlX1V@OIrrl1dR{?Fd zoE&c17JYonanP|to<^-!d)2k39sapak0&o{`vWcQmfCJOPp^C`xlomS^x$#AodFxW zRrIBpYvQtfbQ@iz17vdY^7rPpUwtik!2a!Yd4fx1$Gb}wWQU@zeH6Dk6Kn?holSOa zg3x271F9N_6@s6XB5^f%BIxOz@L2DsUwJ?J`H%PkfQ%+_1%JuS6$S~{D^NDU#RZP+ zB<5V-+1^IvN?$*kpYG4O6Abyk5Ws&hA`15EjNQcNxn=U(AUnyJF!cCjtsj9I zSe3Hr<~Qdrhi%y}pG~~={t6)gDUG8tFq3A(MHgSQD^f9{wX$>pO5a_dA5b|UUHFou zh;p;+&wQ4qo^lTk;`~}-$7a$Q11GLp)O@t0fz#KYuT8ZkT=gf?G|E5LitGc_=<0Me zy7|N7a@pcLT`7uOQBn_ys;yQUfBty2{qedZw_b4%1rysP`@33g`eiZ__ID0iuX5kH zLeJ$noZvgp*CTaQHc2L~B<6NQyV7ovsN#H^d^?f9GL{qH&9{e2BvoymL6C`~sC?gb zs^i6OY`ENX=HMms>DxVC#MueDvlYVjtE|Q@K5uz9{3CtyACKfEOP4!N-78$cq?B@z znOoeMd_NV^{OABG6uhMSa(Z^G^=VPGYX;f3Vns`V9xjE*WC6AL)^n*naS>BO<9gY~ zzhj-?f#&M6K)PB%D6c_94fP#&A0GWOIjv35He*2OUE;V%7sMp-95lEY(idtsV1Jnb z;il*oBw=#O?e6TPVaiO}>CP&@*EmqZwgaP$8oV3|Ly^b`f^n3L`qjM0hH1;|vpwCl zg`iF=ao@=&DzI-_2rMrbGwFzW##_CA8_%4u8ggGXecV8@Yt|L#!`WC@S7)I_b+;5u z)S|Ctcu`=U#}Tc7@ae`AwUY`*_b^R_bDekGHM=OJf5h45k(W4bkRR^;Bt@4wGx_Xo zv8<0WT79KW^qrRy$~LTdx;pp%v;t{@?9a08pIz6!cZN|!ZZRl?Z53q1k}M1|(T=u0 z&h~hccH~IW8cGF6(#SgYC}kXVWF-&w_-I$&mwGT#;REM9eARZGUQr8>r?AKi`EsRLGz;s1y~|*jyT+2ce%F#%WHpJf2p>_=3=*Z(Ow-rL#xh9UcrfQJg71qcVEcd3{+HvMQfIQjc2g zM#Dx%x1kV+Z8cGd*vTl_+J)x4iG>&{!#Q3Q)SmCoHx$@ru1fSg zr+94XTb2J|od=og*2yo)t={eQ0}xv|m3ljYht8u1U!aw`A--Vi!*T+CpJD{|Ph0^v zF8IJ3{2KoUj9r(^~IatdXSW&zseUqKY0@A&RN*XGnPdH*;h|k z6V4?#CVfb*R#B5dnb^PZT4y?x^iaHbVMVX%*l@8`&BvV#4XB-z;s1VvE@Mz%=W4FQ z@`c1(Z*E;A)h;z=qRRf}OTNRGUj2htL+PM={Ggm=WkElaKbq~w#kFDl8z-61{nY|M zC5sa?qg1r~rhzBZ#|l#ec;^IfPg%MEftJ>skH7+LUa}MnXWxl`vx29*zI|772egOU zejA=-e4z%4DfD$3??^*Lk|lVVUGApv{(6r}-+Oh!L~@$ew-@~Ag&us?j(OYcRg>A{{PXve0LK1w4Se#W*?dh=PGX8?j=_@ z8`<3xedlSmD;4ad`hPBbdEm$DA)4N}zgV@LaELx*Bj590#|z1NB>thKHU&31QXg`9 zN4iGM{oFdT->|pK;Ci^E-~0MXw;xnt*#cR$9-9<(yPm;y1US7))@yImFcO|&t};p{ zAzV~jpG=M>uZM~IClzD#asVB<}(VN9U(za!MV=Xcif4$`XT{6!h4fd$$`EDiBb?&|feq zblh#fX7jU}9PVZE%Ltc18wlFmms zpvp^RR6p*QVf0Oh3t{kn0bCnle{X|uqNXQQ)klZrt}-{UGb+e9BK4YHJhP`UO!HR% z#&XgE#O!_;or}mtGzcoTgo~K`GOV1q7sERF?{~%=gx1jcTip7qdX)vA;@0JJ*`rUP z?nd6DNtqK5RGZFGJbQi4*X_1bJYE*&Q zBe5tXbj_IG7uHOAY|710B7lw88eY;GEjEIqaLH~$u5dCeJUpCJt4c<%)c$^9=mJzZ z(1@s<-sWIgmzt^F-yeWJ>m-7Y}JcyJuj86iLIHj)-6dVOazrNV(HS)2x) zu!KdJqvxAF{y9|jvH#JsKkq2p=IIq%74Hfi^>~Z;HbLU7FZ58mDDZ+MM2uguk--WZ zR2u9+`NlDjle2$SjuF@KxOE*nbs-V;L5^@8cK-j@GT=Ov|7O4iyYH-}UApjpGX>75 zcS|y9ncPDEplcGzfQt~}&$hb|{DVdT(1x&4a}3y)&C?+BZ&5No*xgXeMvL^FA$*o! zRRI3kT<2P8EEEe#ljaPbXaC+}m4SPE&=FPS3#ql{J;^AnTaMJPg|hE^m;wq~1@|Ds zo70j$iJLHvoNp>5j$>@&<@;uL`er`@QVr>_EE8D!&mi0U1GuRcdpd-jd~RI9J5zzn z-rmQcmW#^9`D9P1AT!bQij79ny(ImX$*cPZnYh|Jik>_JjtgH`Lj?l=*Bm%UV!i*! zfom$3yN~Y#n1B;g21Tmo=A69eH)3o1HX5QNAdw)G8hKUMTZ9p7&icM#kL7muRiFxo z-NCNI#|I<2?GJ9MLPlXEQN-H8O&xp|F`^r0p|nam^t!osWi~kO9J*}G^`%b{Q*o;X zx9NQ}i-&x%5P3%O$Lz-i}J=F7x^^nFz))2w# zgZ9~(k)iCz^DS>zdmd;sIXO9LwVwY@BVwOuQH<-VL&q|8J(gXm)qyv_SgV?u_E%P8 zr z$hi|hplpw;)50>Ze!oa-h~0V@(ggeR{h_8&2JH~MVnGY}t@|!MXg1zE&Gv*lri4@E zz4tHpWj2PcolU%{e>O@4>gNy61HCtbP~J3?uVbxc?kFML+>EzYEzt_?46 zCM}5p-)lZl#0~1F0kQ*A)HoUIu}RzV_JWazGF2FhrODK)Yj0+{f4y+ueLh+JO4p5< z!cVvQgb4(m@MoX&H7z`eNcLzXxM03N;zn9;J<}r~1WB!9HUWfa0Q(SBeP(nrH z&Z&^3Q{unb+S)#tI{Mb12k$_*mDtDWfppw8c5XlHWpdjM4BZYl#lKRXeIA~}Z}`{` z2w$_D&~CRcCwI1;Td(q zv)A7$1!OG(dityKPZWf`QsE9V1e!J@_EQ+wsk`*<8b!gQPujW`C1#~g z>bz(#G>5rn-O~CAfPo*;XMT^A^?GkCoNWm;zmvI4W$a^}p(;7e$tUE+J~8`G&L*Vf zIlidv+mVlkKN=oiMAC{_uveAJl`BJ{lY!PyI7K8ZCoTAVWoyf6#&p1{&khuh%|k_K z&^zwtKl&wfB>FsD<+1kyac^wYEzD)024EObQ??asiSoeOa$o#p4$#zLDE?WYArNv5 zLPbabuS&XtI@t(VgH*j@9#j25snJ^lvJuumk6x^(tc=dK*1ZBrV#L?>!lC%yZ^@J@ zMEyBSm_+PxtE#FHx+dSWlPP@O`6ON@)qk?mzMA1bUgBLUITYe-<_oJ!^Yd(a&+f;6 zvWMOps_@lc%6kQ*B{5MdhVx*w*oGM@Ipq|cxp=3lg7|iB>n24=*5iSH=yCGR%%N!Ss2CL<-av;e>h~ zpr-u9oR%;E{ce487gSckCO7$#tW3duadx9hj6%4iXNQ4W$6EfI+LS9RwhNQ+|CItS^5fyV^riFjxdP9l~lk)ZXLtM%}w&|_MMOM zW^wPU;4O2qczzYnetf*Hkw(AIIU4ql1tkmGAXWNjzZSPb0TXduLYX)}IMK{o{| z>zSZCW2#?9yulaViu)Yzu+N~$5aLXPu=xZKl@dJ`jVcR*E|F{m5oPRTgC^uxHh@Y+ z;E<*u;3S1@5@t@?q#b0wNJ7JZmIy6IZuPSRg1tXV@{ruwRiGdPr5YCEao`_}Pi?OC zoygY2tlmZ@>hIVV&x>SfU=RM78}hR^2KAtZqPc3v41% zcPtL4^?^tLa!n=K9cSkXY1hvDj&3t*oBAv=ugCuNvKF11f}s#LyTD5fs*)2RsI=(v zxnCS5%feGDu%^TDfFM|=WxX^+RNWAi|7J7B$s7Mlz5Vn)X$V?_OQ!6iBMif!ibNq! za+gmrVA~O9f4Ti%1(^#BflhgvCI3pz1xd56wqoY)3DJ}zDt<&SNytVVUo(t1L5H6G z;R^OOTWEUWGPGL>9lY^`+XwqH611Cs3S~w^w~#zcte?dPL#KQ6c(8@Y!r^8xZk6T(k&3{)%@wLyljl5FSW^ekl@%!0ELxZGO!G z;Y2r<4s0|h*`IU)7G30vICxeZHMCmv6LS@2_*&7VERiaZm5a!1Sr;;UQOJgNKe2ro zgqy#FNC1j{wft38+Lw!z=<``jOmDRCFVW=PC-6qJ+Tx+GGJic1SOhzG;h+N3_J2O{ z#x{W&_THyn9dQ<64j{i_fLQ08uvkdPge z^A-0xWME}wHKAx$zi>CkJ0;~jWK|#)e>JjnDFG(&6HWq!(zxKLya2q5 zsUww=1Guaahmpb3BW^m9(Je3`0R}z*h?@@(Hz~?~Jq6pvuLU2+@1q5Ea6aC2PSr0L zgrH$UL3VDbJejb=90i2Qat>9X0(0P{4+}Vm1w$l?$oGSTN%>6Lp6+hUTWaAbe(;XK z24|?mZW7;hYsq>#R6P2xePJW6AV)X8r1b;FC8ibUrTB#COe8n`J5dKtxH!eHKmV7Y z9%=fkA_htUTl8`~n(uEna429R`pj_&BAzE`E_q-5$2hU*!ncO1JlvF`gkOnM*p^5` zVW7*F{oDb?ekA|cG#9Xm(WcyAUxyDQFc=je@gKF%78&y$<2Vgt;Lr1mI>Ig=(H}xAn8U3l4txC3T`+16PAv@ z58{bn5_&-v0B(7k>Q$F@la*+{w}cWy-DE#^d2WbAJ}Js``T^4Piygav9;5?O%67ld z)B~|tRFa{Y^$93~zEGri5K{59$a+}x$ZS5<|3tPZ9k<{g8-DjZR)OC<;{I_@Vru== zz=Lm#7NdrUnu0Ur=16)x|KCNSD`uigcDI&;DK6ZOv*=4tg+R|$Sy{WE)5?ha0??+q zeo$IPxI`k!>2pfnk&%&{Mi3VHTIp7tuI{M+nzje#5*t#}bbaQOF_+%p)paPudOd1N>jqNKr}p9cOgEfJgj-Qj0cBb7lz^?mlXcOk zf=afOhi6aRe2Ol9zzCIcUJ2XX+i)UG1F8*Giz*7sf_cp(?(99?9+_cz7PmoTbiTIG zs4-}}dUHu!Fg-nRa@&aB&$}exkXKSOpi-jMLxD(Pu~=R_ZHeXr3=%V?mV5&H{OpN~jZ*NrIZaa?NKI`^4c zbnDwg$sbkCIh_(t9Z(1BqJThO6;x6s4TXYH`9X|gLr}s=JcqeC}(DbETmP0FA?t&PiUY<04mP_t`) zOg2E$tq-NOQzHCOdxmvWB~`W{M#6 z?S-zgo0QM7NZ*A}ID}8%*OWNSYmT{9ef-#qW1yoG5un6K^chUaS`ocP&I8-%E9S2W z6d8dUF1YrBN(ajPT1XZ_UGp)QG|PePA}CHwS^yr!I%h_DQaVxfS_{|aTt?2WNP4e< z$3`Dh5h@RLscNxkc;T)3tyNE`urW5=rF%y|V>5nbI8FV0YisL^6t@KB#;dm0>YhA7 z$n$$kD-p>tpefI4==2?~rt5Cbbl3UKL9OCS`?SV2AixSl9!~e-YN51cS%oz4P&4Yb z`uWB;+7(rIc6R!UZSLg62=*F`VAAJOvGY$JAe5OPti4?6tgiH9SqcXtA|j4+X(>Zc z$7klCVj;iPYI9+5bgWCQ{LXU4&!Eb^rE(^8u-K9?X&zl2)M=Oa+%IX;_HNvWR`ijX z(k**3v1_&@6PewnxMb4DuyA!XStc-ayP8-M=d`}|F#CT>Pv`XF&aqhdXx?K5#ADU8 zhLpN+-+L5_jhL7?3+8BYOBVJ7l$^Heij(m?gat3*K4<44y9jlq$1?Ij6Y}$zj#!CB z`#dn_5>FjLrPQCTKi`T^@tV>bOsFXo9A2q`SfcB9se7M~S06%AcXyABj&9+P8q%!m zP_b*HFyqFJ8wL@5L)vgE$j)>m$fbtH&cr>>%Z{JIX6Qb}QX=2@O7qv_lRZ7}BTt}3 zVi2la`83fX*3~$tOIKgt8Wbe!E2WQSZ{Hz`qfTDuzsRhrtx>Yj74Jtd0CBQyIvu<( z<8=Xh1B$bF=}(5#%^<}5G|QlJ2y7M=6}sQl;7{~yAzY4C*3@8#6*29@1_I=vHe=EUH7nY2dEN~ z#;1>yp(7I-OJN1T_dll`s}+#^A+VBgrUc)Ui1t?#$FS9?j}vFRiW$)3+Ol-@W7pT$ zS1HmHAgc!X0vS2{c4*}0n11@m2VpWZE*}Wry%Y4};NURJ;IY;JJ!8q9%J}4BbTu@L zFI>0~78yzJr=O(w?2M|e?p%4~)Q=zMTS92h|7lW=g^UVa$o00!qP?SI^P;Krxwu~3v^2JIC;HvRk58uxm7I&h#*v35_-71VU<}(-PBF^;^HIpL+R(j!ev)P( zgRYiVIvxG*H&{ySNq_ep(t)^Fy)kr8%|E>P^Fr_$uGZ4u8^DZ{hB3mqto(@lpU=E8 zEfT-_{`UYJhi70(es=53KcA7IO9}qGDm)1`tF$f2=PTVe{ya2T5MQpXKZ6T*m1e;` zp6&DQoanQEK9jbcVnPhf??#S4hIv8OBX|DPKc5}X{)Y4K1N)G%l;GccKXdl)r0|xm ztaShkqN%24*w@$Rh^fKKNKd~?S;bqoiZU~~Tie>K@+`YA{kafV3j@K@ z+qdr_i)(%TQEPMas&uW7v?B2gbz*)#Uq)u;$KGBIFyWT7ieDuEUZupsU6TGKeI+D1 z+M1S@7K@%}B#_|mlsWn$HzmMi^7b&bM zE?z7I!cPa#CR^wC$-gtL6J}arZf=)+AYJ{Mw{y(uv6srN_@MtW$HT+Jnz^X|XNZ0J zktLxbb?{MQr3NRy@M9V*edV1}XIW$h zk^C9FxQP}Cvj3bSUFp~wXuZ(L7uPtU0gdOKtiRd~5yn1+r?qIZzC9T{JZw`PEzEc7 zCdL|d`82koyl5t^YLY>nVp2-Tl8AO!P|-(-Db;7)Nuuw>;`e#DMWlJac&1>E9xF0OB`A%YYCc*J==ZD^n8_zuUR~t7W?lmlR zv~L7|SL?gZCKcVS#ok*e=v;Wp7L+>pY<@g|xyUF;q#-{)f1zwXyWo!JPQB;$I6?Vp z1HHt`tXXR15Th2Jb|ib0>s*>6z!5O1IU^u|aRi7n9P4#xK2)Zu_vCPQKF4t(uhG@3 zFf{8^7jV%FEnGS4)9YK256YT(Dpsz#!G68Z!1yED$)H{S#B$YMcJQ@wkqe1)qNY)H zPwtqmU}`t}^=xz+gBTAgc&oNQ8Mv=rJF4pzhu-+PFHHYy@%T7Xm}%IeR@|o}Oc$P= zkICil6z{3pnWlGJD*YkO!Q}G&Rgpy>Cl@d87A85j*lLi^ZLx4*AvGar?T9l`a=lBI z30+ud<>**+usLkw*vRO#E#h^sq2;&ON0JTYygvdg`D&gLZ8ATQQ+z=^`Ro49&q~tJ z&`_pJ4jh;mz<9~+lypSdNbuiddpuzC`x})Z_+fssbUwRWT$`(_5&I}Tg9Mj z7&u}32krruS+<&N2sD`LWqX7-0GoO*ZWGi>+a3Lxl$6ATZf$9?;0*n*fDZ*{HTP** z9_mn{QI;x_JI2HL_g@5nf8jI)(QRXD{YS5~ROyMbp^|J_aGKlJl7Yu!PfGC7GMpJP z2$AehS5wo=hq}$%-wSA>P!3H+iA3WTiUZ%x%3wjvI!67rYtj|JAsz+Q2H)%ScM`7a zD{-O_8%GeZl;l{kO{|uTY%PkiU$uz@#YKrri5!J95j^Vf{l^bI5VixXF09?0!yf>_ zg$!l9_N`I3PEJlv?(^px!^u-$r+cOy88y7jqkm9Rd$?77wBwS#dy?)-6F97k ztqdPOj`yfYZTu9-2EpK$BAc-#-aGy$@#bNxcXiM*)c45oIKe2|TepixjV8<4Qp$Mw z0yD(fhQ|Hf=hB4G=dxp*aW@mN6An|;)6)}}V!6a`MGCev4_5=^I^_3PN6Cme%o+Rp z`}c$Qn&ZxCe3D+UO*Wxpz{tyg1Wuq3`WiEe(K1`;&x+L2P{*bz zEkcFXt?&do3zc#ErV!S{Q1y6)bqgq5P4N;AR#_gu&HLPZB<)bya*(OnTDR0tZmy3c!&Ddh{s>R&paKqZQ8pi1&%)lF zyMS^WUQ@k~;u#d--djS(ku`2TnM`zkhGve@WbDG@lzErH|`yl7zJ<; z_9^&|{r>U?9x&{p7f9q~0bGz=8~VL(vsA%GzbX)G$4}>L`Avx(N9NZS&td^Dm#X$f z^$P4N-Mxtyf44Cq3%ja?0=G3;jgPb*v6&IU;+KZsFch}1$GD$-GaQS*2&Mw;KhD}^ zf<5G81^E}6s;XTk9+*#mH%&KW(@afmx_$dLH?T^Zt~Z0p<)!szEs6{F_xB&u^5b0l zjWc}eh+#hjE@N9WYjAL|>KYdTcGkr>{qC05){C&b`1twt_SVp(zxoXe_ntPg=f|?M zvsa-kU0urp2*OA{uPoH74SW=ruD*iO)YP@GNS~UTs_&kmA^HOpjE$#$cd6d};CU{j*VT5cSfd^RE7bhNM~gp&RoA z+13gEJPVn$_w)Y%RJM4jCHPKDGa`TP%MzjrUHtg{1N;|%cYjQ6fYsuY zBWwRbgSC_Z&i=o8nEw_B-sGUbA1=c-H#ZM^`_{O*xj8H}^nOG{MAk4?_LDyy1p|8qK}WI z+3;>EDp~_~6c!sh*2KRf{AV;i7f5T{UgYNf2D=1BP|$80azCsuu~Sz_N3JLNk@GNP)@4m_j$`=p2|5sm7ONzwKB9v_;QFHU6W}V+oGG zSZqEYyUq`lWevwXwxUNI2J}xAg?wdZD>=8h&+q!jpq(~*WP1S2O@-_BOhWJwNPTQz zId1}0Hw=V=_0j&%;1RE*1IO8<@F8D3VgP}X1#CgWA56|?1CR1GNMxD@mTEo#nGVmL zDe66dh6@e!HXkjsOB?nERNV^2awNq8JBCF?Em&8;{6_qkO>}9vQJLraphEe8Gcg-BJ2kUggH8ZtMm12 zD*Ft)4jhCq+cq$aPRIYuuU^XkJy4V#hdtNy+O=zk$K8Q1g(>_1faZsQCMZWcXqi)K zqQL0z_DG9$BR+s>4QhklFDNL0yLlFfEtS-Ds+Cx#R+iOn#JDZx0Xu=1Lyx`1;>})l zl|i8BGhu5l0#=iB08&lX4^51XowvUgT80x$LXz#isV(ZLw+b5(>MEBDn~MV$+Z>6a zXxoSoTdwFCmRkJSvoLVSOTZcv7vlcSu5qUnQc+fI8ATg{nQgT69YSMUW&ZUceHswx zQb@S~$u3!tmShlmf-(JSwq9`uv!Bhkl5U)+^yfsbZkr3MEh#5a=L@&o&CSyYioxL{ zjS!`H>b5aAWi5p8Ysjf!UKBY-P>Z+7tozJ7B*C;dLh``87_aqUemz$rwSjZ|QR&2U zu7$5g!4g)ssL1otk6s5a`coU^0#~EXs*iX_g?#}0f97^KXvxJ zzq^sP8UGLgP%bVmfsskL`&TFjaHAeQdHHKa!m+z&PG%F4r)Q>5q=vtAU7s13ahUl0 zOb}8Ey|}Aw#w1Cd<1y((mK@)oq{k6GB+7jAs zK)&agllbH$-d#K;iBALM-hg~RADQX+N#;$2Z_Pc8pw5Mk&gXh|{nokg^-`DGfV5{pDZGc$3_XK`CGEFh8w`WkH~oA36&P%Bkbopzal( zknV%hssI_?JR~dAS0)0$Z_f0Y-n$|fopS+)S+|vzl33nJt)`QOOG{r z0d-X>``2q6tVNNe=BKrM_}_MI*{g|row7?yw|;$d^YY>6z$8$s*$_A5()lel4zGPg z^{h)?JUF-&cuC?ux3I<%;7rNCt$|U^w*soU)>i$@1THxQjuvj)@L-i)X6@QTo3z$I zM+>F2YQv@`?X_7pKep^V+>{aRitNLDD{I~P@yw}McCOd#vE8Gy7Av)pev|}r$ zZ**E1a?pD|=iWI>FRWRxEEPQN8-Qi+ZdlnnOC)CYp=AO)laKQOcLdx~Tz{kHQAhpumAh6uH^#kg5WQr{r|(c;yGL+7w3&91MJY<_g2)sX;6n7vv#tS; zi>`heo?R}QbQ^f7pUAlu>-Gn;3WCoyKx!O|H4fdZ*AK_dQ0PK5YT`n zK{b4klKzMwXf*J3EdJ3zYe!&FuGr!*n^VT3-~e=tB=;J-9wa9*Fl|%^4e}ihfz-Xn z;{0C*9^#lXZQ3urEZ~qjr*Rrv7qk{^2Ms^odg~dzH7z~;x7b=4jxE~`0H;~j?%8t( z*ecL5Hr~8d|NK{ILQ!aA1vX57MFs^uir%_x`SR+DgqsdL=i_8beNQug@eWQ+UAlDH zvZ%PYIGNtQg|HgcgoTfDT~^4{>s?)3tJkeFW9-72E9mN;t#|Z-j8|!` iy`v6od Date: Tue, 22 Oct 2019 20:12:43 +0200 Subject: [PATCH 11/85] Minor changes to wiring --- kappa.js | 47 +++++++++++++++++++++++++++-------------------- 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/kappa.js b/kappa.js index d5f2392..9264945 100644 --- a/kappa.js +++ b/kappa.js @@ -37,18 +37,18 @@ module.exports = class Kappa extends EventEmitter { use (name, view) { const self = this view.name = name - view.api = view.api || {} - this.views[name] = view - this.api[name] = { - ready (cb) { - self.onViewIndexed(name, cb) - } - } - for (let [key, value] of Object.entries(view.api)) { - if (typeof value === 'function') value = value.bind(this.api[name], this) - this.api[name][key] = value + if (view.api) { + this.api[name] = { + ready (cb) { + self.onViewIndexed(name, cb) + } + } + for (let [key, value] of Object.entries(view.api)) { + if (typeof value === 'function') value = value.bind(this.api[name], this) + this.api[name][key] = value + } } if (this.opts.autoconnect || view.autoconnect) { @@ -278,18 +278,25 @@ class Flow extends EventEmitter { process.nextTick(this._run.bind(this)) } - _onbatch (msgs, cb) { - const { source, view } = this - - const steps = [ - source.transform, - view.filter, - view.transform - ].filter(fn => fn) + // collectParents () { + // let cur = this + // let list = [cur] + // while (cur.parent) { + // list.push(cur.parent) + // cur = cur.parent + // } + // return list + // } + _onbatch (msgs, cb) { + let steps = [ + this.source.transform, + this.view.filter, + this.view.transform + ].filter(f => f) runAll(msgs, steps, msgs => { if (!msgs.length) cb(null, msgs) - else view.map(msgs, () => cb(null, msgs)) + else this.view.map(msgs, () => cb(null, msgs)) }) } @@ -338,7 +345,7 @@ class Flow extends EventEmitter { _onsource (name, createSource, opts) { const fullname = this.source.name + ':' + name - opts.parent = this.name + opts.parent = this this.kappa.source(fullname, createSource, opts) const flow = this.kappa.connect(fullname, this.view.name) this.subflows.push(flow) From a21b44cd7f26548de8cefd40a741caa46377ebd1 Mon Sep 17 00:00:00 2001 From: "Franz Heinzmann (Frando)" Date: Tue, 22 Oct 2019 20:14:11 +0200 Subject: [PATCH 12/85] Add a hyperdrive source handler & test --- package.json | 3 + sources/hypercore.js | 5 +- sources/hyperdrive.js | 127 ++++++++++++++++++++++++++++++++++++++++++ test/hyperdrive.js | 67 ++++++++++++++++++++++ test/kappa-new.js | 1 + test/lib/util.js | 13 +++++ 6 files changed, 215 insertions(+), 1 deletion(-) create mode 100644 sources/hyperdrive.js create mode 100644 test/hyperdrive.js create mode 100644 test/lib/util.js diff --git a/package.json b/package.json index 561fb6e..7769bfe 100644 --- a/package.json +++ b/package.json @@ -19,9 +19,12 @@ "inherits": "^2.0.4", "multifeed": "^4.0.0", "multifeed-index": "^3.3.2", + "stream-collector": "^1.0.1", "thunky": "^1.1.0" }, "devDependencies": { + "hyperdrive": "^10", + "hyperdrive-schemas": "^0.12.0", "random-access-memory": "^3.1.1", "standard": "~12.0.1", "tape": "^4.11.0" diff --git a/sources/hypercore.js b/sources/hypercore.js index cdfacd0..861d318 100644 --- a/sources/hypercore.js +++ b/sources/hypercore.js @@ -2,7 +2,10 @@ module.exports = function hypercoreSource (handlers, opts) { const maxBatch = opts.maxBatch || 50 const feed = opts.feed - return { open, pull } + // TODO: Maybe to this in the flow class, passing on opts? + const transform = opts.transform + + return { open, pull, transform } function open (next) { feed.ready(function () { diff --git a/sources/hyperdrive.js b/sources/hyperdrive.js new file mode 100644 index 0000000..264f76f --- /dev/null +++ b/sources/hyperdrive.js @@ -0,0 +1,127 @@ +const hypercoreSource = require('./hypercore') +const hyperdrive = require('hyperdrive') +// const Stat = require('hyperdrive/lib/stat') +const collect = require('stream-collector') +const p = require('path') +// const { Node } = require('hypertrie/lib/messages') + +// TODO: Handle adding/removing mounts on the fly. +module.exports = function hyperdriveSource (handlers, opts) { + const { + drive, + maxMountDepth = 5, + isolateMounts = false, + maxBatch = 100, + _mountDepth = 0, + _mountPath, + _rootKey + } = opts + + let key = null + return { open, pull, transform } + + function open (next) { + drive.ready(() => { + drive.metadata.setMaxListeners(256) + key = drive.key.toString('hex') + drive.watch('/', handlers.onupdate) + next() + }) + } + + function pull (state, next) { + const at = state || 0 + const to = Math.min(at + maxBatch, drive.version) + const diffStream = drive.checkout(to).createDiffStream(at) + collect(diffStream, (err, res) => { + next(to, res, to < drive.version) + diffStream.destroy() + }) + } + + function transform (msgs, next) { + let pending = 1 + for (const node of msgs) { + const path = node.name + + if (node.type === 'mount') { + ++pending + drive.stat(path, (err, stat) => { + if (err || !stat || !stat.mount) return done() + else { + node.mount = stat.mount + mountToSource(path, stat.mount, done) + } + }) + } + // TODO: Unmounts. + + if (!isolateMounts && _mountPath) { + node.name = p.join(_mountPath, path) + node.key = _rootKey + } else { + node.key = key + } + } + + done() + + function done () { + if (--pending === 0) next(msgs) + } + } + + function mountToSource (path, mountInfo, cb) { + if (_mountDepth >= maxMountDepth) return cb() + const { key, hypercore } = mountInfo + // TODO: Support hypercore mounts. + if (hypercore) return cb() + const subdrive = hyperdrive(drive._corestore, key) + const hexkey = key.toString('hex') + handlers.onsource(hexkey, hyperdriveSource, { + ...opts, + drive: subdrive, + _mountDepth: _mountDepth + 1, + _mountPath: p.join(_mountPath || '', path), + _rootKey: _rootKey || key + }) + cb(null, mountInfo) + } +} + + + + + + + + + + + // function onmetadatafeed (feed) { + // const name = feed.key.toString('hex') + // handlers.onsource(name, hypercoreSource, { + // feed, + // transform + // }) + // } + + // function transform (msgs, next) { + // console.log('TRANSFORM', msgs.length) + // msgs = msgs.map(msg => { + // if (msg.seq === 0) return + // console.log('pre', msg) + // const node = Node.decode(msg.value) + // console.log('NODE', {...node}) + // const stat = Stat.decode(node.valueBuffer) + // msg.value = stat + // console.log('post', msg) + // return { + // key: msg.key, + // path: node.key, + // seq: msg.seq, + // value: stat + // } + // }).filter(m => m) + // next(msgs) + // } diff --git a/test/hyperdrive.js b/test/hyperdrive.js new file mode 100644 index 0000000..e8b74da --- /dev/null +++ b/test/hyperdrive.js @@ -0,0 +1,67 @@ +const tape = require('tape') +const ram = require('random-access-memory') +const hyperdrive = require('hyperdrive') +const corestore = require('corestore') +const { runAll } = require('./lib/util') + +const { Kappa } = require('..') +const hyperdriveSource = require('../sources/hyperdrive') + +tape.only('hyperdrive source', async t => { + const cstore = corestore(ram) + const drive1 = hyperdrive(cstore) + const drive2 = hyperdrive(cstore) + const drive3 = hyperdrive(cstore) + + const kappa = new Kappa({ autostart: true, autoconnect: true }) + let list = [] + kappa.use('files', { + map (msgs, next) { + list.push(...msgs) + next() + }, + api: { + collect (kappa, cb) { + this.ready(async () => { + console.log('KIST', list) + const res = list.map(node => `${node.type} ${node.name}`) + cb(null, res) + }) + } + } + }) + + kappa.source('drive', hyperdriveSource, { drive: drive1, fileContentPromise: true }) + + await runAll([ + cb => drive1.ready(cb), + cb => drive2.ready(cb), + cb => drive3.ready(cb), + + cb => drive1.mount('in2', drive2.key, cb), + cb => drive2.mount('in3', drive3.key, cb), + + cb => drive1.writeFile('hello', Buffer.from('world1'), cb), + cb => drive2.writeFile('hello', Buffer.from('world2'), cb), + cb => drive3.writeFile('hello', Buffer.from('world3'), cb), + + cb => drive1.unlink('hello', cb) + ]) + + kappa.api.files.collect((err, res) => { + t.error(err) + res.sort() + t.deepEqual(res, [ + 'del hello', + 'mount in2', + // TODO: I don't know why "mount in2" is here two times. + 'mount in2', + 'mount in2/in3', + 'put hello', + 'put in2/hello', + 'put in2/in3/hello' + ]) + console.log('res', res) + t.end() + }) +}) diff --git a/test/kappa-new.js b/test/kappa-new.js index 231f2c6..1c32cc4 100644 --- a/test/kappa-new.js +++ b/test/kappa-new.js @@ -3,6 +3,7 @@ const { Kappa } = require('..') const hypercore = require('hypercore') const ram = require('random-access-memory') const hypercoreSource = require('../sources/hypercore') +// const { runAll } = require('./lib/util') function kappacore () { return new Kappa({ autoconnect: true, autostart: true }) diff --git a/test/lib/util.js b/test/lib/util.js new file mode 100644 index 0000000..ce8027c --- /dev/null +++ b/test/lib/util.js @@ -0,0 +1,13 @@ +exports.runAll = function (ops) { + return new Promise((resolve, reject) => { + runNext(ops.shift()) + function runNext (op) { + op(err => { + if (err) return reject(err) + let next = ops.shift() + if (!next) return resolve() + return runNext(next) + }) + } + }) +} From 400d16b64a16b8e9cfba30ca454a7f496fc03cee Mon Sep 17 00:00:00 2001 From: "Franz Heinzmann (Frando)" Date: Tue, 22 Oct 2019 20:34:33 +0200 Subject: [PATCH 13/85] Enable all tests --- test/hyperdrive.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/hyperdrive.js b/test/hyperdrive.js index e8b74da..7e8526d 100644 --- a/test/hyperdrive.js +++ b/test/hyperdrive.js @@ -7,7 +7,7 @@ const { runAll } = require('./lib/util') const { Kappa } = require('..') const hyperdriveSource = require('../sources/hyperdrive') -tape.only('hyperdrive source', async t => { +tape('hyperdrive source', async t => { const cstore = corestore(ram) const drive1 = hyperdrive(cstore) const drive2 = hyperdrive(cstore) From f4ae3342ce42e2481b8b8dd9e2c0070e62b24e09 Mon Sep 17 00:00:00 2001 From: "Franz Heinzmann (Frando)" Date: Tue, 22 Oct 2019 21:41:35 +0200 Subject: [PATCH 14/85] Use svg for flow graph --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 320c95a..298986b 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ In `index.js` this new, dependencyless Kappa is wired to multifeed, creating an More docs come soon. -![kappa flow](kappa-flow.png) +![kappa flow](kappa-flow.svg) --- From a58dffcf5fd5b9569a19536a84c05355ea07b8e4 Mon Sep 17 00:00:00 2001 From: "Franz K.H" Date: Tue, 22 Oct 2019 21:42:36 +0200 Subject: [PATCH 15/85] Added kappa-graph.svg --- kappa-graph.svg | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 kappa-graph.svg diff --git a/kappa-graph.svg b/kappa-graph.svg new file mode 100644 index 0000000..e89eb12 --- /dev/null +++ b/kappa-graph.svg @@ -0,0 +1,3 @@ + + +
kappa.source
(name, createSource. opts)
kappa.source<br><i>(name, createSource. opts)</i>
kappa.view
(name, handlers)
kappa.view<br><i>(name, handlers)</i>
kappa.connect
(sourcename, viewname)
kappa.connect<br><i>(sourcename, viewname)</i>
view: viewname
[Not supported by viewer]
flow: sourcename~viewname
flow: <i style="font-weight: normal">sourcename~viewname</i>
ready()
ready()
pause()
pause()
resume()
resume()
state
state
source: sourcename
[Not supported by viewer]
createSource()
createSource()
pull(state)
pull(<i>state</i>)
transform(msgs)
transform(<i>msgs</i>)
filter(msgs)
filter(<i>msgs</i>)
transform(msgs)
transform(<i>msgs</i>)
map(msgs)
map(<i>msgs</i>)
source instance
source instance
kappa.source
(name, createSource, opts)
kappa.source<br><i>(name, createSource, opts)</i>
source: sourcename
[Not supported by viewer]
createSource()
createSource()
kappa.view
(name, handlers)
kappa.view<br><i>(name, handlers)</i>
view: viewname
[Not supported by viewer]
map(msgs)
map(<i>msgs</i>)
api
api
api
api
kappa.ready()
kappa.ready()
kappa.pause()
kappa.pause()
kappa.resume()
kappa.resume()
\ No newline at end of file From 03665c15c4c94fd1d4408de4dde8e2edff0c8c77 Mon Sep 17 00:00:00 2001 From: "Franz Heinzmann (Frando)" Date: Tue, 22 Oct 2019 21:42:57 +0200 Subject: [PATCH 16/85] Use svg for flow graph --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 298986b..7c0192a 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ In `index.js` this new, dependencyless Kappa is wired to multifeed, creating an More docs come soon. -![kappa flow](kappa-flow.svg) +![kappa flow](kappa-graph.svg) --- From 7203eda6b3ddb0cf0aa4b037305821dd89b29478 Mon Sep 17 00:00:00 2001 From: "Franz Heinzmann (Frando)" Date: Tue, 22 Oct 2019 21:44:06 +0200 Subject: [PATCH 17/85] Remove png graph --- kappa-flow.png | Bin 81862 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 kappa-flow.png diff --git a/kappa-flow.png b/kappa-flow.png deleted file mode 100644 index 1e634bcb0ea984330b7eff17747591698a227dcb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 81862 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 iy`v6od Date: Wed, 23 Oct 2019 03:25:34 +0200 Subject: [PATCH 18/85] Default opts, and cleanup --- README.md | 2 +- index.js | 2 +- kappa.js | 8 ++- package.json | 5 +- sources/hyperdrive.js | 83 ++++++++++++-------------- test/hyperdrive.js | 136 +++++++++++++++++++++++++++++------------- test/kappa-new.js | 9 +-- 7 files changed, 147 insertions(+), 98 deletions(-) diff --git a/README.md b/README.md index 7c0192a..89d4a4a 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Make the base Kappa core dependencyless. See `./kappa.js`. `Kappa` just deals with *sources* and *views*. Sources pull values based on their last state, views process values in a map function. -`kappa.source(sourceName, createSource, opts)` registers a new source. `createSource` is invoked after the source is connected to a view and should return an object with at least `pull` handler. +`kappa.source(sourceName, createSource, opts)` registers a new source. `createSource` is invoked after the source is connected to a view and should return an object with at least a `pull` handler. `kappa.use(viewName, view)` registers a new view (same as in kappa-core v4). diff --git a/index.js b/index.js index 8b40bfd..16a8682 100644 --- a/index.js +++ b/index.js @@ -10,7 +10,7 @@ function kappaClassic (storage, opts = {}) { const feeds = opts.multifeed || multifeed(hypercore, storage, opts) - const kappa = new Kappa({ autoconnect: true, autostart: true }) + const kappa = new Kappa() kappa.source('multifeed', multifeedSource, { feeds }) diff --git a/kappa.js b/kappa.js index 9264945..a0d2729 100644 --- a/kappa.js +++ b/kappa.js @@ -14,7 +14,11 @@ module.exports = class Kappa extends EventEmitter { */ constructor (opts = {}) { super() - this.opts = opts + this.opts = { + autoconnect: true, + autostart: true, + ...opts + } this.views = {} this.sources = {} this.api = {} @@ -201,7 +205,7 @@ class Flow extends EventEmitter { onupdate: this._onupdate.bind(this), onerror: this._onerror.bind(this), onsource: this._onsource.bind(this) - }, opts) + }, opts, this.kappa) if (!this.source.name) this.source.name = opts.name this.name = this.source.name + '~' + this.view.name diff --git a/package.json b/package.json index 7769bfe..5d5133e 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ }, "keywords": [], "dependencies": { - "hypercore": "^7.4.0", + "hypercore": "^7", "inherits": "^2.0.4", "multifeed": "^4.0.0", "multifeed-index": "^3.3.2", @@ -23,8 +23,11 @@ "thunky": "^1.1.0" }, "devDependencies": { + "bitfield-db": "^1.2.2", + "corestore": "2", "hyperdrive": "^10", "hyperdrive-schemas": "^0.12.0", + "nanoiterator": "^1.2.0", "random-access-memory": "^3.1.1", "standard": "~12.0.1", "tape": "^4.11.0" diff --git a/sources/hyperdrive.js b/sources/hyperdrive.js index 264f76f..68700c6 100644 --- a/sources/hyperdrive.js +++ b/sources/hyperdrive.js @@ -32,10 +32,10 @@ module.exports = function hyperdriveSource (handlers, opts) { function pull (state, next) { const at = state || 0 const to = Math.min(at + maxBatch, drive.version) + if (to <= at) return next() const diffStream = drive.checkout(to).createDiffStream(at) collect(diffStream, (err, res) => { next(to, res, to < drive.version) - diffStream.destroy() }) } @@ -49,7 +49,7 @@ module.exports = function hyperdriveSource (handlers, opts) { drive.stat(path, (err, stat) => { if (err || !stat || !stat.mount) return done() else { - node.mount = stat.mount + // node.mount = stat.mount mountToSource(path, stat.mount, done) } }) @@ -78,50 +78,43 @@ module.exports = function hyperdriveSource (handlers, opts) { if (hypercore) return cb() const subdrive = hyperdrive(drive._corestore, key) const hexkey = key.toString('hex') - handlers.onsource(hexkey, hyperdriveSource, { - ...opts, - drive: subdrive, - _mountDepth: _mountDepth + 1, - _mountPath: p.join(_mountPath || '', path), - _rootKey: _rootKey || key + subdrive.ready(() => { + handlers.onsource(hexkey, hyperdriveSource, { + ...opts, + drive: subdrive, + _mountDepth: _mountDepth + 1, + _mountPath: p.join(_mountPath || '', path), + _rootKey: _rootKey || key + }) + cb(null, mountInfo) }) - cb(null, mountInfo) } } - - - - - - - - - - // function onmetadatafeed (feed) { - // const name = feed.key.toString('hex') - // handlers.onsource(name, hypercoreSource, { - // feed, - // transform - // }) - // } - - // function transform (msgs, next) { - // console.log('TRANSFORM', msgs.length) - // msgs = msgs.map(msg => { - // if (msg.seq === 0) return - // console.log('pre', msg) - // const node = Node.decode(msg.value) - // console.log('NODE', {...node}) - // const stat = Stat.decode(node.valueBuffer) - // msg.value = stat - // console.log('post', msg) - // return { - // key: msg.key, - // path: node.key, - // seq: msg.seq, - // value: stat - // } - // }).filter(m => m) - // next(msgs) - // } +// function onmetadatafeed (feed) { +// const name = feed.key.toString('hex') +// handlers.onsource(name, hypercoreSource, { +// feed, +// transform +// }) +// } + +// function transform (msgs, next) { +// console.log('TRANSFORM', msgs.length) +// msgs = msgs.map(msg => { +// if (msg.seq === 0) return +// console.log('pre', msg) +// const node = Node.decode(msg.value) +// console.log('NODE', {...node}) +// const stat = Stat.decode(node.valueBuffer) +// msg.value = stat +// console.log('post', msg) +// return { +// key: msg.key, +// path: node.key, +// seq: msg.seq, +// value: stat +// } +// }).filter(m => m) +// next(msgs) +// } diff --git a/test/hyperdrive.js b/test/hyperdrive.js index 7e8526d..b14f61d 100644 --- a/test/hyperdrive.js +++ b/test/hyperdrive.js @@ -3,65 +3,119 @@ const ram = require('random-access-memory') const hyperdrive = require('hyperdrive') const corestore = require('corestore') const { runAll } = require('./lib/util') +const crypto = require('hypercore-crypto') const { Kappa } = require('..') const hyperdriveSource = require('../sources/hyperdrive') tape('hyperdrive source', async t => { const cstore = corestore(ram) - const drive1 = hyperdrive(cstore) - const drive2 = hyperdrive(cstore) - const drive3 = hyperdrive(cstore) + var drive1, drive2, drive3, driveB, kappa, kappaB - const kappa = new Kappa({ autostart: true, autoconnect: true }) - let list = [] - kappa.use('files', { - map (msgs, next) { - list.push(...msgs) - next() + await runAll([ + // init all the drives + cb => { + const keyPair = crypto.keyPair() + drive1 = hyperdrive(cstore, keyPair.publicKey, { keyPair }) + drive1.ready(cb) + }, + cb => { + const keyPair = crypto.keyPair() + drive2 = hyperdrive(cstore, keyPair.publicKey, { keyPair }) + drive2.ready(cb) + }, + cb => { + const keyPair = crypto.keyPair() + drive3 = hyperdrive(cstore, keyPair.publicKey, { keyPair }) + drive3.ready(cb) }, - api: { - collect (kappa, cb) { - this.ready(async () => { - console.log('KIST', list) - const res = list.map(node => `${node.type} ${node.name}`) - cb(null, res) - }) - } - } - }) - - kappa.source('drive', hyperdriveSource, { drive: drive1, fileContentPromise: true }) - await runAll([ - cb => drive1.ready(cb), - cb => drive2.ready(cb), - cb => drive3.ready(cb), + // open first kappa + cb => { + kappa = new Kappa() + kappa.use('files', makeFilesView()) + kappa.source('drive', hyperdriveSource, { drive: drive1 }) + kappa.pause() + cb() + }, + // mounts and writeFiles + cb => drive1.writeFile('hello1', Buffer.from('world1'), cb), + cb => drive2.writeFile('hello2', Buffer.from('world2'), cb), + cb => drive3.writeFile('hello3', Buffer.from('world3'), cb), cb => drive1.mount('in2', drive2.key, cb), cb => drive2.mount('in3', drive3.key, cb), + cb => drive1.unlink('hello1', cb), - cb => drive1.writeFile('hello', Buffer.from('world1'), cb), - cb => drive2.writeFile('hello', Buffer.from('world2'), cb), - cb => drive3.writeFile('hello', Buffer.from('world3'), cb), + // pause the kappa so that intermediate diffs are skipped + // in a real-world scenario the views would have be aware + // of the linear, but not synonym history of hyperdrives + // (when using the diff iterator) + cb => { + kappa.resume() + cb() + }, - cb => drive1.unlink('hello', cb) + // open second (remote) drive + cb => { + driveB = hyperdrive(ram, drive1.key) + driveB.ready(cb) + }, + + // init second (remote) kappa + cb => { + kappaB = new Kappa() + kappaB.use('files', makeFilesView()) + kappaB.source('drive', hyperdriveSource, { drive: driveB }) + cb() + }, + + // replicate the local and remote drives + cb => { + replicate(drive1, driveB) + setTimeout(cb, 300) + }, ]) + const expected = [ + 'mount in2', + 'mount in2/in3', + 'put in2/hello2', + 'put in2/in3/hello3' + ] + kappa.api.files.collect((err, res) => { t.error(err) res.sort() - t.deepEqual(res, [ - 'del hello', - 'mount in2', - // TODO: I don't know why "mount in2" is here two times. - 'mount in2', - 'mount in2/in3', - 'put hello', - 'put in2/hello', - 'put in2/in3/hello' - ]) - console.log('res', res) - t.end() + t.deepEqual(res, expected, 'original kappa matches') + kappaB.api.files.collect((err, res) => { + t.error(err) + res.sort() + t.deepEqual(res, expected, 'replicated kappa matches') + t.end() + }) }) }) + +function makeFilesView () { + let list = [] + return { + map (msgs, next) { + list.push(...msgs) + next() + }, + api: { + collect (kappa, cb) { + this.ready(async () => { + const res = list.map(node => `${node.type} ${node.name}`) + cb(null, res) + }) + } + } + } +} + +function replicate (a, b) { + const stream = a.replicate(true, { live: true }) + stream.pipe(b.replicate(false, { live: true })).pipe(stream) +} diff --git a/test/kappa-new.js b/test/kappa-new.js index 1c32cc4..85ee3b4 100644 --- a/test/kappa-new.js +++ b/test/kappa-new.js @@ -3,14 +3,9 @@ const { Kappa } = require('..') const hypercore = require('hypercore') const ram = require('random-access-memory') const hypercoreSource = require('../sources/hypercore') -// const { runAll } = require('./lib/util') - -function kappacore () { - return new Kappa({ autoconnect: true, autostart: true }) -} tape('simple source', t => { - const kappa = kappacore() + const kappa = new Kappa() const [source1, pushTo1] = makeSimpleSource() const [source2, pushTo2] = makeSimpleSource() @@ -45,7 +40,7 @@ tape('simple source', t => { }) tape('hypercore source', t => { - const kappa = kappacore() + const kappa = new Kappa() const core1 = hypercore(ram, { valueEncoding: 'utf8' }) const core2 = hypercore(ram, { valueEncoding: 'utf8' }) From 1a281b896028666a45f1996dc5e923b9f1cc680f Mon Sep 17 00:00:00 2001 From: "Franz Heinzmann (Frando)" Date: Thu, 24 Oct 2019 16:09:49 +0200 Subject: [PATCH 19/85] Remove subflows for now --- kappa.js | 78 ++++++++++-------------------------- package.json | 2 +- sources/multifeed.js | 79 +++++++++++++++++++++++++++++-------- sources/util/multisource.js | 43 ++++++++++++++++++++ test/lib/util.js | 10 ++++- 5 files changed, 135 insertions(+), 77 deletions(-) create mode 100644 sources/util/multisource.js diff --git a/kappa.js b/kappa.js index a0d2729..3dfa75b 100644 --- a/kappa.js +++ b/kappa.js @@ -55,7 +55,7 @@ module.exports = class Kappa extends EventEmitter { } } - if (this.opts.autoconnect || view.autoconnect) { + if (this.opts.autoconnect) { Object.keys(this.sources).forEach(sourceName => this.connect(sourceName, name)) this.on('source', sourceName => this.connect(sourceName, name)) } @@ -73,9 +73,9 @@ module.exports = class Kappa extends EventEmitter { source (name, createSource, opts = {}) { if (this.sources[name]) return opts.name = name - this.sources[name] = { createSource, opts, name } + this.sources[name] = { name, createSource, opts } - if (this.opts.autoconnect || opts.autoconnect) { + if (this.opts.autoconnect) { Object.keys(this.views).forEach(viewName => this.connect(name, viewName)) this.on('view', viewName => this.connect(name, viewName)) } @@ -202,57 +202,42 @@ class Flow extends EventEmitter { this.opts = opts this.source = createSource({ - onupdate: this._onupdate.bind(this), - onerror: this._onerror.bind(this), - onsource: this._onsource.bind(this) + onupdate: this._onupdate.bind(this) }, opts, this.kappa) if (!this.source.name) this.source.name = opts.name this.name = this.source.name + '~' + this.view.name this.parent = opts.parent - this.subflows = [] this.status = Status.Ready this._opened = false this.open = thunky(this._open.bind(this)) } - _open (cb) { + _open (cb = noop) { if (this._opened) return cb() const self = this - let pending = this.subflows.length + 1 - + let pending = 1 if (this.view.open) ++pending && this.view.open(finish) if (this.source.open) ++pending && this.source.open(finish) - this.subflows.forEach(flow => flow.open(finish)) - finish() function finish () { - if (!(--pending === 0)) return - // self.status = Status.Ready + if (--pending !== 0) return self._opened = true self._run() - if (cb) cb() + cb() } } ready (cb = noop) { const self = this - if (!this._opened) { - return this.open(() => this.ready(cb)) - } + if (!this._opened) return this.open(() => this.ready(cb)) - let pending = this.subflows.length + 1 - this.subflows.forEach(flow => flow.ready(finish)) - finish() - function finish () { - if (--pending !== 0) return - if (self.status === Status.Ready) process.nextTick(cb) - else self.once('ready', cb) - } + if (self.status === Status.Ready) process.nextTick(cb) + else self.once('ready', cb) } pause () { @@ -266,12 +251,12 @@ class Flow extends EventEmitter { else this._run() } - restart (cb) { + restart (cb = noop) { this.pause() process.nextTick(() => { this.kappa._storeState(this, null, () => { this.resume() - if (cb) cb() + cb() }) }) } @@ -282,16 +267,6 @@ class Flow extends EventEmitter { process.nextTick(this._run.bind(this)) } - // collectParents () { - // let cur = this - // let list = [cur] - // while (cur.parent) { - // list.push(cur.parent) - // cur = cur.parent - // } - // return list - // } - _onbatch (msgs, cb) { let steps = [ this.source.transform, @@ -313,7 +288,7 @@ class Flow extends EventEmitter { this.status = Status.Running this.kappa._fetchState(this, (err, state) => { - if (err) return this._onerror(err) + if (err) return close(err) this.source.pull(state, onbatch) }) @@ -321,19 +296,19 @@ class Flow extends EventEmitter { msgs = msgs || [] if (self.status === Status.Paused) return close() self._onbatch(msgs, (err, msgs) => { - if (err) return self._onerror(err) + if (err) return close(err) self.kappa._storeState(self, nextState, err => { - if (err) return self._onerror(err) - close(msgs, workMore) + close(err, msgs, workMore) }) }) } - function close (msgs, workMore) { - self.status = Status.Ready - if (msgs.length && self.view.indexed) { + function close (err, msgs, workMore) { + if (err) self.kappa.emit(err, this) + else if (msgs.length && self.view.indexed) { self.view.indexed(msgs) } + self.status = Status.Ready if (self.incomingUpdate || workMore) { self.incomingUpdate = false process.nextTick(self._run.bind(self)) @@ -342,19 +317,6 @@ class Flow extends EventEmitter { } } } - - _onerror (err) { - this.kappa.emit(err, this) - } - - _onsource (name, createSource, opts) { - const fullname = this.source.name + ':' + name - opts.parent = this - this.kappa.source(fullname, createSource, opts) - const flow = this.kappa.connect(fullname, this.view.name) - this.subflows.push(flow) - flow.open() - } } function runAll (state, fns, final) { diff --git a/package.json b/package.json index 5d5133e..427290e 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,6 @@ }, "keywords": [], "dependencies": { - "hypercore": "^7", "inherits": "^2.0.4", "multifeed": "^4.0.0", "multifeed-index": "^3.3.2", @@ -25,6 +24,7 @@ "devDependencies": { "bitfield-db": "^1.2.2", "corestore": "2", + "hypercore": "^8", "hyperdrive": "^10", "hyperdrive-schemas": "^0.12.0", "nanoiterator": "^1.2.0", diff --git a/sources/multifeed.js b/sources/multifeed.js index fc809a1..4555738 100644 --- a/sources/multifeed.js +++ b/sources/multifeed.js @@ -1,31 +1,76 @@ const hypercoreSource = require('./hypercore') +const multi = require('./util/multisource') module.exports = function multifeedSource (handlers, opts) { - const feeds = opts.feeds + const { pull, addSource } = multi(handlers, { open }) + const multifeed = opts.feeds - return { open, pull } - - function pull (state, next) { - next(state) - } + return { pull, open } function open (next) { - feeds.ready(() => { - let pending = feeds.feeds().length + 1 - feeds.feeds().forEach(feed => _onfeed(feed, done)) - feeds.on('feed', feed => _onfeed(feed)) - done() - function done () { - if (!--pending) next() - } + multifeed.ready(() => { + multifeed.feeds().forEach(feed => _onfeed(feed)) + multifeed.on('feed', feed => _onfeed(feed)) + next() }) } function _onfeed (feed, cb) { feed.ready(() => { - const name = feed.key.toString('hex') - handlers.onsource(name, hypercoreSource, { feed }) - if (cb) cb() + addSource(feed.key.toString('hex'), hypercoreSource, { feed }) }) } } + +// module.exports = function multifeedSource (handlers, opts) { +// const multifeed = opts.feeds +// const sources = {} +// const pullwanted = new Set() + +// return { open, pull } + +// function pull (state = {}, next) { +// if (!pullwanted.size) return next(state) + +// let messages = [] +// let pending = pullwanted.size +// for (let key of pullwanted) { +// pullwanted.delete(key) +// sources[key].pull(state[key], finish.bind(finish, key)) +// } + +// function finish (key, nextState, msgs, moreWork) { +// if (moreWork) pullwanted.add(key) +// state[key] = nextState +// if (msgs) messages = messages.concat(msgs) +// if (--pending === 0) { +// next(state, messages, pullwanted.size > 0) +// } +// } +// } + +// function open (next) { +// multifeed.ready(() => { +// multifeed.feeds().forEach(feed => _onfeed(feed)) +// multifeed.on('feed', feed => _onfeed(feed)) +// next() +// }) +// } + +// function _onfeed (feed, cb) { +// feed.ready(() => { +// const name = feed.key.toString('hex') +// const source = hypercoreSource({ onupdate: () => _onfeedupdate(name) }, { feed }) +// sources[name] = source +// source.open(() => { +// _onfeedupdate(name) +// if (cb) cb() +// }) +// }) +// } + +// function _onfeedupdate (name) { +// pullwanted.add(name) +// handlers.onupdate() +// } +// } diff --git a/sources/util/multisource.js b/sources/util/multisource.js new file mode 100644 index 0000000..e4031f2 --- /dev/null +++ b/sources/util/multisource.js @@ -0,0 +1,43 @@ +module.exports = function multiSource (handlers, opts) { + const sources = {} + const updateWanted = new Set() + + return { pull, addSource, sources } + + function addSource (name, createSource, opts, cb) { + if (sources[name]) return + const handlers = { + onupdate: onupdate.bind(onupdate, name) + } + const source = createSource(handlers, opts) + source.name = name + sources[name] = source + source.open(() => { + onupdate(name) + if (cb) cb() + }) + } + + function onupdate (name) { + updateWanted.add(name) + handlers.onupdate() + } + + function pull (state, next) { + state = state || {} + if (!updateWanted.size) return next(state) + + let results = [] + let pending = updateWanted.size + for (let name of updateWanted) { + updateWanted.delete(name) + sources[name].pull(state[name], done.bind(done, name)) + } + function done (name, nextState, msgs, moreWork) { + if (moreWork) updateWanted.add(name) + if (msgs) results = results.concat(msgs) + state[name] = nextState + if (--pending === 0) next(state, results, updateWanted.size) + } + } +} diff --git a/test/lib/util.js b/test/lib/util.js index ce8027c..6dd1adb 100644 --- a/test/lib/util.js +++ b/test/lib/util.js @@ -1,4 +1,4 @@ -exports.runAll = function (ops) { +exports.runAll = function runAll (ops) { return new Promise((resolve, reject) => { runNext(ops.shift()) function runNext (op) { @@ -11,3 +11,11 @@ exports.runAll = function (ops) { } }) } + +exports.replicate = function replicate (a, b, opts, cb) { + if (typeof opts === 'function') return replicate(a, b, null, opts) + if (!opts) opts = { live: true } + const stream = a.replicate(true, opts) + stream.pipe(b.replicate(false, opts)).pipe(stream) + setImmediate(cb) +} From c996fe6d74f40b70f67b4f1f420b0830fe35e2f7 Mon Sep 17 00:00:00 2001 From: "Franz Heinzmann (Frando)" Date: Thu, 24 Oct 2019 16:10:52 +0200 Subject: [PATCH 20/85] Fix hyperdrive, improve tests --- .gitignore | 1 + sources/hyperdrive.js | 140 ++++++++++++++++++++---------------------- test/basic.js | 36 ----------- test/hyperdrive.js | 69 +++++++++++---------- test/multifeed.js | 58 +++++++++++++++++ 5 files changed, 161 insertions(+), 143 deletions(-) delete mode 100644 test/basic.js create mode 100644 test/multifeed.js diff --git a/.gitignore b/.gitignore index d5f19d8..d01fca5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules package-lock.json +pnpm-lock.yaml diff --git a/sources/hyperdrive.js b/sources/hyperdrive.js index 68700c6..cdf8d6e 100644 --- a/sources/hyperdrive.js +++ b/sources/hyperdrive.js @@ -1,23 +1,26 @@ -const hypercoreSource = require('./hypercore') const hyperdrive = require('hyperdrive') -// const Stat = require('hyperdrive/lib/stat') const collect = require('stream-collector') const p = require('path') -// const { Node } = require('hypertrie/lib/messages') -// TODO: Handle adding/removing mounts on the fly. +const multi = require('./util/multisource') + module.exports = function hyperdriveSource (handlers, opts) { + if (opts.mount === false) { + return hyperdriveSingle(handlers, opts) + } else { + return hyperdriveMounts(handlers, opts) + } +} + +function hyperdriveSingle (handlers, opts) { const { drive, - maxMountDepth = 5, - isolateMounts = false, maxBatch = 100, - _mountDepth = 0, - _mountPath, - _rootKey + _mountInfo } = opts - let key = null + var key + return { open, pull, transform } function open (next) { @@ -34,87 +37,76 @@ module.exports = function hyperdriveSource (handlers, opts) { const to = Math.min(at + maxBatch, drive.version) if (to <= at) return next() const diffStream = drive.checkout(to).createDiffStream(at) - collect(diffStream, (err, res) => { - next(to, res, to < drive.version) + collect(diffStream, (err, msgs) => { + transform(msgs, msgs => { + next(to, msgs, to < drive.version) + }) }) } function transform (msgs, next) { - let pending = 1 for (const node of msgs) { - const path = node.name - - if (node.type === 'mount') { - ++pending - drive.stat(path, (err, stat) => { - if (err || !stat || !stat.mount) return done() - else { - // node.mount = stat.mount - mountToSource(path, stat.mount, done) - } - }) - } - // TODO: Unmounts. - - if (!isolateMounts && _mountPath) { - node.name = p.join(_mountPath, path) - node.key = _rootKey + if (_mountInfo) { + node.name = p.join(_mountInfo.path, node.name) + node.mountInfo = { + key, + path: _mountInfo.path + } + node.key = _mountInfo.key } else { node.key = key } } + next(msgs) + } +} - done() +function hyperdriveMounts (handlers, opts) { + const { pull, addSource } = multi(handlers, { open }) + const { drive } = opts + + return { pull, open, transform } + function open (next) { + drive.ready(() => { + addSource(drive.key.toString('hex'), hyperdriveSingle, { drive }, next) + }) + } + + function transform (msgs, next) { + let pending = 1 + for (const node of msgs) { + if (node.type === 'mount') { + ++pending + mountToSource(node, done) + } + } + done() function done () { if (--pending === 0) next(msgs) } } - function mountToSource (path, mountInfo, cb) { - if (_mountDepth >= maxMountDepth) return cb() - const { key, hypercore } = mountInfo - // TODO: Support hypercore mounts. - if (hypercore) return cb() - const subdrive = hyperdrive(drive._corestore, key) - const hexkey = key.toString('hex') - subdrive.ready(() => { - handlers.onsource(hexkey, hyperdriveSource, { - ...opts, - drive: subdrive, - _mountDepth: _mountDepth + 1, - _mountPath: p.join(_mountPath || '', path), - _rootKey: _rootKey || key + function mountToSource (node, done) { + const path = node.name + drive.stat(path, (err, stat) => { + if (err || !stat || !stat.mount) return done() + const { key, hypercore } = stat.mount + // TODO: Support hypercore mounts? + if (hypercore) return done() + + const subdrive = hyperdrive(drive._corestore, key) + subdrive.ready(() => { + addSource(key.toString('hex'), hyperdriveSingle, { + ...opts, + drive: subdrive, + _mountInfo: { + key: node.key, + path + } + }) + done() }) - cb(null, mountInfo) }) } } - -// function onmetadatafeed (feed) { -// const name = feed.key.toString('hex') -// handlers.onsource(name, hypercoreSource, { -// feed, -// transform -// }) -// } - -// function transform (msgs, next) { -// console.log('TRANSFORM', msgs.length) -// msgs = msgs.map(msg => { -// if (msg.seq === 0) return -// console.log('pre', msg) -// const node = Node.decode(msg.value) -// console.log('NODE', {...node}) -// const stat = Stat.decode(node.valueBuffer) -// msg.value = stat -// console.log('post', msg) -// return { -// key: msg.key, -// path: node.key, -// seq: msg.seq, -// value: stat -// } -// }).filter(m => m) -// next(msgs) -// } diff --git a/test/basic.js b/test/basic.js deleted file mode 100644 index f3247c7..0000000 --- a/test/basic.js +++ /dev/null @@ -1,36 +0,0 @@ -var test = require('tape') -var ram = require('random-access-memory') -var kappa = require('..') - -test('simple view', function (t) { - var core = kappa(ram, { valueEncoding: 'json' }) - - var sum = 0 - - var sumview = { - api: { - get: function (core, cb) { - this.ready(function () { - cb(null, sum) - }) - } - }, - map: function (msgs, next) { - msgs.forEach(function (msg) { - if (typeof msg.value === 'number') sum += msg.value - }) - next() - } - } - - core.use('sum', sumview) - - core.writer('default', function (err, feed) { - feed.append(1, function (err) { - core.api.sum.get(function (err, value) { - t.equals(1, value) - t.end() - }) - }) - }) -}) diff --git a/test/hyperdrive.js b/test/hyperdrive.js index b14f61d..cfabb6a 100644 --- a/test/hyperdrive.js +++ b/test/hyperdrive.js @@ -2,31 +2,36 @@ const tape = require('tape') const ram = require('random-access-memory') const hyperdrive = require('hyperdrive') const corestore = require('corestore') -const { runAll } = require('./lib/util') +const { runAll, replicate } = require('./lib/util') const crypto = require('hypercore-crypto') const { Kappa } = require('..') const hyperdriveSource = require('../sources/hyperdrive') -tape('hyperdrive source', async t => { +tape.only('hyperdrive source', async t => { const cstore = corestore(ram) var drive1, drive2, drive3, driveB, kappa, kappaB + const expected = [ + 'put hello1', + 'mount in2', + 'mount in2/in3', + 'put in2/hello2', + 'put in2/in3/hello3' + ] + await runAll([ // init all the drives cb => { - const keyPair = crypto.keyPair() - drive1 = hyperdrive(cstore, keyPair.publicKey, { keyPair }) + drive1 = hyperdrive(cstore) drive1.ready(cb) }, cb => { - const keyPair = crypto.keyPair() - drive2 = hyperdrive(cstore, keyPair.publicKey, { keyPair }) + drive2 = hyperdrive(cstore) drive2.ready(cb) }, cb => { - const keyPair = crypto.keyPair() - drive3 = hyperdrive(cstore, keyPair.publicKey, { keyPair }) + drive3 = hyperdrive(cstore) drive3.ready(cb) }, @@ -41,12 +46,24 @@ tape('hyperdrive source', async t => { // mounts and writeFiles cb => drive1.writeFile('hello1', Buffer.from('world1'), cb), + cb => drive1.writeFile('hello1_del', Buffer.from('world1'), cb), cb => drive2.writeFile('hello2', Buffer.from('world2'), cb), cb => drive3.writeFile('hello3', Buffer.from('world3'), cb), cb => drive1.mount('in2', drive2.key, cb), cb => drive2.mount('in3', drive3.key, cb), - cb => drive1.unlink('hello1', cb), + cb => drive1.unlink('hello1_del', cb), + + // resume kappa so that the indexes are built + cb => { + kappa.resume() + cb() + }, + + cb => testResult(kappa, 'original kappa matches', cb) + ]) + + await runAll([ // pause the kappa so that intermediate diffs are skipped // in a real-world scenario the views would have be aware // of the linear, but not synonym history of hyperdrives @@ -71,30 +88,21 @@ tape('hyperdrive source', async t => { }, // replicate the local and remote drives - cb => { - replicate(drive1, driveB) - setTimeout(cb, 300) - }, + cb => replicate(drive1, driveB, cb), + + cb => testResult(kappaB, 'replicated kappa matches', cb) ]) - const expected = [ - 'mount in2', - 'mount in2/in3', - 'put in2/hello2', - 'put in2/in3/hello3' - ] + t.end() - kappa.api.files.collect((err, res) => { - t.error(err) - res.sort() - t.deepEqual(res, expected, 'original kappa matches') - kappaB.api.files.collect((err, res) => { + function testResult (kappa, msg, cb) { + if (typeof msg === 'function') return testResult(kappa, '', msg) + kappa.api.files.collect((err, res) => { t.error(err) - res.sort() - t.deepEqual(res, expected, 'replicated kappa matches') - t.end() + t.deepEqual(res, expected, msg) + cb() }) - }) + } }) function makeFilesView () { @@ -114,8 +122,3 @@ function makeFilesView () { } } } - -function replicate (a, b) { - const stream = a.replicate(true, { live: true }) - stream.pipe(b.replicate(false, { live: true })).pipe(stream) -} diff --git a/test/multifeed.js b/test/multifeed.js new file mode 100644 index 0000000..17672d2 --- /dev/null +++ b/test/multifeed.js @@ -0,0 +1,58 @@ +const test = require('tape') +const ram = require('random-access-memory') +const kappa = require('..') +const { runAll } = require('./lib/util') + +test('multifeed', async t => { + const core = kappa(ram, { valueEncoding: 'json' }) + + function createSumView () { + let sum = 0 + const sumview = { + api: { + get: function (core, cb) { + this.ready(function () { + cb(null, sum) + }) + } + }, + map: function (msgs, next) { + msgs.forEach(function (msg) { + if (typeof msg.value === 'number') sum += msg.value + }) + next() + } + } + return sumview + } + + core.use('sum', createSumView()) + + var feed1, feed2 + + await runAll([ + cb => core.writer('default', (err, feed) => (feed1 = feed, cb())), + cb => core.writer('second', (err, feed) => (feed2 = feed, cb())), + cb => feed1.append(1, cb), + cb => feed1.append(1, cb), + cb => feed2.append(3, cb), + cb => { + core.api.sum.get(function (err, value) { + t.equals(5, value) + cb() + }) + } + ]) + + const core2 = kappa(ram, { valueEncoding: 'json' }) + + t.end() +}) + +function replicate (a, b, opts, cb) { + if (typeof opts === 'function') return replicate(a, b, null, cb) + if (!opts) opts = { live: true } + const stream = a.replicate(opts) + stream.pipe(b.replicate(opts)).pipe(stream) + setImmediate(cb) +} From c32347ee35c7fb7c73bbfd312c1e0c97f896c75e Mon Sep 17 00:00:00 2001 From: "Franz Heinzmann (Frando)" Date: Thu, 24 Oct 2019 16:26:45 +0200 Subject: [PATCH 21/85] Tests & cleanup --- sources/multifeed.js | 55 +-------------------------- test/basic.js | 75 +++++++++++++++++++++++++++++++++++++ test/hypercore.js | 38 +++++++++++++++++++ test/hyperdrive.js | 2 +- test/kappa-new.js | 88 -------------------------------------------- test/multifeed.js | 40 ++++++++++---------- 6 files changed, 135 insertions(+), 163 deletions(-) create mode 100644 test/basic.js create mode 100644 test/hypercore.js delete mode 100644 test/kappa-new.js diff --git a/sources/multifeed.js b/sources/multifeed.js index 4555738..31934fc 100644 --- a/sources/multifeed.js +++ b/sources/multifeed.js @@ -2,7 +2,7 @@ const hypercoreSource = require('./hypercore') const multi = require('./util/multisource') module.exports = function multifeedSource (handlers, opts) { - const { pull, addSource } = multi(handlers, { open }) + const { pull, addSource } = multi(handlers) const multifeed = opts.feeds return { pull, open } @@ -21,56 +21,3 @@ module.exports = function multifeedSource (handlers, opts) { }) } } - -// module.exports = function multifeedSource (handlers, opts) { -// const multifeed = opts.feeds -// const sources = {} -// const pullwanted = new Set() - -// return { open, pull } - -// function pull (state = {}, next) { -// if (!pullwanted.size) return next(state) - -// let messages = [] -// let pending = pullwanted.size -// for (let key of pullwanted) { -// pullwanted.delete(key) -// sources[key].pull(state[key], finish.bind(finish, key)) -// } - -// function finish (key, nextState, msgs, moreWork) { -// if (moreWork) pullwanted.add(key) -// state[key] = nextState -// if (msgs) messages = messages.concat(msgs) -// if (--pending === 0) { -// next(state, messages, pullwanted.size > 0) -// } -// } -// } - -// function open (next) { -// multifeed.ready(() => { -// multifeed.feeds().forEach(feed => _onfeed(feed)) -// multifeed.on('feed', feed => _onfeed(feed)) -// next() -// }) -// } - -// function _onfeed (feed, cb) { -// feed.ready(() => { -// const name = feed.key.toString('hex') -// const source = hypercoreSource({ onupdate: () => _onfeedupdate(name) }, { feed }) -// sources[name] = source -// source.open(() => { -// _onfeedupdate(name) -// if (cb) cb() -// }) -// }) -// } - -// function _onfeedupdate (name) { -// pullwanted.add(name) -// handlers.onupdate() -// } -// } diff --git a/test/basic.js b/test/basic.js new file mode 100644 index 0000000..5f2d2f2 --- /dev/null +++ b/test/basic.js @@ -0,0 +1,75 @@ +const tape = require('tape') +const { Kappa } = require('..') +const { runAll } = require('./lib/util') + +tape('simple source', t => { + const kappa = new Kappa() + + const [source1, pushTo1] = makeSimpleSource() + const [source2, pushTo2] = makeSimpleSource() + kappa.source('s1', source1) + kappa.source('s2', source2) + kappa.use('view1', makeSimpleView()) + kappa.use('view2', makeSimpleView()) + + pushTo1(1) + pushTo1(2) + pushTo2(3) + pushTo2(4) + + runAll([ + cb => kappa.api.view1.collect((err, res) => { + t.error(err) + t.deepEqual(res, [1, 2, 3, 4]) + cb() + }), + cb => kappa.api.view2.collect((err, res) => { + t.error(err) + t.deepEqual(res, [1, 2, 3, 4]) + cb() + }), + cb => t.end() + ]) +}) + + +function makeSimpleView () { + let res = [] + const view = { + map (msgs, next) { + res = res.concat(msgs) + next() + }, + api: { + collect (kappa, cb) { + this.ready(() => cb(null, res)) + } + } + } + return view +} + +function makeSimpleSource () { + const buf = [] + const listeners = [] + + return [createSource, push] + + function createSource (handlers, opts) { + listeners.push(handlers.onupdate) + const maxBatch = opts.maxBatch || 2 + return { + pull (state, next) { + state = state || 0 + const end = Math.min(state + maxBatch, buf.length) + const slice = buf.slice(state, end) + next(end, slice, end < buf.length) + } + } + } + + function push (value) { + buf.push(value) + listeners.forEach(onupdate => onupdate()) + } +} diff --git a/test/hypercore.js b/test/hypercore.js new file mode 100644 index 0000000..d1beacb --- /dev/null +++ b/test/hypercore.js @@ -0,0 +1,38 @@ +const tape = require('tape') +const { Kappa } = require('..') +const hypercore = require('hypercore') +const ram = require('random-access-memory') +const hypercoreSource = require('../sources/hypercore') + +tape('hypercore source', t => { + const kappa = new Kappa() + + const core1 = hypercore(ram, { valueEncoding: 'json' }) + const core2 = hypercore(ram, { valueEncoding: 'json' }) + kappa.source('core1', hypercoreSource, { feed: core1 }) + kappa.source('core2', hypercoreSource, { feed: core2 }) + + let res = [] + kappa.use('view', { + map (msgs, next) { + res = res.concat(msgs.map(msg => msg.value)) + next() + }, + api: { + collect (kappa, cb) { + this.ready(() => cb(null, res)) + } + } + }) + + core1.append(1) + core2.append(2) + core1.append(3) + + setImmediate(() => { + kappa.api.view.collect((err, res) => { + t.deepEqual(res, [1, 2, 3]) + t.end() + }) + }) +}) diff --git a/test/hyperdrive.js b/test/hyperdrive.js index cfabb6a..0716b69 100644 --- a/test/hyperdrive.js +++ b/test/hyperdrive.js @@ -8,7 +8,7 @@ const crypto = require('hypercore-crypto') const { Kappa } = require('..') const hyperdriveSource = require('../sources/hyperdrive') -tape.only('hyperdrive source', async t => { +tape('hyperdrive source', async t => { const cstore = corestore(ram) var drive1, drive2, drive3, driveB, kappa, kappaB diff --git a/test/kappa-new.js b/test/kappa-new.js deleted file mode 100644 index 85ee3b4..0000000 --- a/test/kappa-new.js +++ /dev/null @@ -1,88 +0,0 @@ -const tape = require('tape') -const { Kappa } = require('..') -const hypercore = require('hypercore') -const ram = require('random-access-memory') -const hypercoreSource = require('../sources/hypercore') - -tape('simple source', t => { - const kappa = new Kappa() - - const [source1, pushTo1] = makeSimpleSource() - const [source2, pushTo2] = makeSimpleSource() - - kappa.source('s1', source1) - kappa.source('s2', source2) - - kappa.use('view', { - map (msgs, next) { - console.log('[view:map]', msgs) - next() - } - }) - - kappa.use('other', { - map (msgs, next) { - console.log('[other:map]', msgs) - next() - } - }) - - pushTo1('one1') - pushTo1('one2') - setTimeout(() => { - pushTo2('two1') - pushTo2('two2') - }, 100) - - console.log(kappa.flows.map(f => f.name)) - - setTimeout(() => t.end(), 1000) -}) - -tape('hypercore source', t => { - const kappa = new Kappa() - - const core1 = hypercore(ram, { valueEncoding: 'utf8' }) - const core2 = hypercore(ram, { valueEncoding: 'utf8' }) - kappa.source('core1', hypercoreSource, { feed: core1 }) - kappa.source('core2', hypercoreSource, { feed: core2 }) - - kappa.use('view', { - map (msgs, next) { - console.log('[view:map]', msgs) - next() - } - }) - - core1.append('one1') - core1.append('one2') - core2.append('two1') - core2.append('two2') - - setTimeout(() => t.end(), 1000) -}) - -function makeSimpleSource () { - const buf = [] - const listeners = [] - - return [createSource, push] - - function createSource (handlers, opts) { - listeners.push(handlers.onupdate) - const maxBatch = opts.maxBatch || 2 - return { - pull (state, next) { - state = state || 0 - const end = Math.min(state + maxBatch, buf.length) - const slice = buf.slice(state, end) - next(end, slice, end < buf.length) - } - } - } - - function push (value) { - buf.push(value) - listeners.forEach(onupdate => onupdate()) - } -} diff --git a/test/multifeed.js b/test/multifeed.js index 17672d2..cb9126d 100644 --- a/test/multifeed.js +++ b/test/multifeed.js @@ -6,26 +6,6 @@ const { runAll } = require('./lib/util') test('multifeed', async t => { const core = kappa(ram, { valueEncoding: 'json' }) - function createSumView () { - let sum = 0 - const sumview = { - api: { - get: function (core, cb) { - this.ready(function () { - cb(null, sum) - }) - } - }, - map: function (msgs, next) { - msgs.forEach(function (msg) { - if (typeof msg.value === 'number') sum += msg.value - }) - next() - } - } - return sumview - } - core.use('sum', createSumView()) var feed1, feed2 @@ -49,6 +29,26 @@ test('multifeed', async t => { t.end() }) +function createSumView () { + let sum = 0 + const sumview = { + api: { + get: function (core, cb) { + this.ready(function () { + cb(null, sum) + }) + } + }, + map: function (msgs, next) { + msgs.forEach(function (msg) { + if (typeof msg.value === 'number') sum += msg.value + }) + next() + } + } + return sumview +} + function replicate (a, b, opts, cb) { if (typeof opts === 'function') return replicate(a, b, null, cb) if (!opts) opts = { live: true } From 11cfe8c68bd5540444bbd95d77d30a9a4bb3d2e0 Mon Sep 17 00:00:00 2001 From: "Franz Heinzmann (Frando)" Date: Thu, 24 Oct 2019 16:30:18 +0200 Subject: [PATCH 22/85] Lint standard --- sources/hyperdrive.js | 1 + test/basic.js | 1 - test/hypercore.js | 1 + test/hyperdrive.js | 4 +--- test/multifeed.js | 29 ++++++++++++++++++----------- 5 files changed, 21 insertions(+), 15 deletions(-) diff --git a/sources/hyperdrive.js b/sources/hyperdrive.js index cdf8d6e..4a3ecd3 100644 --- a/sources/hyperdrive.js +++ b/sources/hyperdrive.js @@ -38,6 +38,7 @@ function hyperdriveSingle (handlers, opts) { if (to <= at) return next() const diffStream = drive.checkout(to).createDiffStream(at) collect(diffStream, (err, msgs) => { + if (err) return next(state) transform(msgs, msgs => { next(to, msgs, to < drive.version) }) diff --git a/test/basic.js b/test/basic.js index 5f2d2f2..56d6fca 100644 --- a/test/basic.js +++ b/test/basic.js @@ -32,7 +32,6 @@ tape('simple source', t => { ]) }) - function makeSimpleView () { let res = [] const view = { diff --git a/test/hypercore.js b/test/hypercore.js index d1beacb..9789f05 100644 --- a/test/hypercore.js +++ b/test/hypercore.js @@ -31,6 +31,7 @@ tape('hypercore source', t => { setImmediate(() => { kappa.api.view.collect((err, res) => { + t.error(err) t.deepEqual(res, [1, 2, 3]) t.end() }) diff --git a/test/hyperdrive.js b/test/hyperdrive.js index 0716b69..c41116e 100644 --- a/test/hyperdrive.js +++ b/test/hyperdrive.js @@ -3,7 +3,6 @@ const ram = require('random-access-memory') const hyperdrive = require('hyperdrive') const corestore = require('corestore') const { runAll, replicate } = require('./lib/util') -const crypto = require('hypercore-crypto') const { Kappa } = require('..') const hyperdriveSource = require('../sources/hyperdrive') @@ -62,7 +61,6 @@ tape('hyperdrive source', async t => { cb => testResult(kappa, 'original kappa matches', cb) ]) - await runAll([ // pause the kappa so that intermediate diffs are skipped // in a real-world scenario the views would have be aware @@ -89,7 +87,7 @@ tape('hyperdrive source', async t => { // replicate the local and remote drives cb => replicate(drive1, driveB, cb), - + cb => testResult(kappaB, 'replicated kappa matches', cb) ]) diff --git a/test/multifeed.js b/test/multifeed.js index cb9126d..b87812c 100644 --- a/test/multifeed.js +++ b/test/multifeed.js @@ -11,21 +11,28 @@ test('multifeed', async t => { var feed1, feed2 await runAll([ - cb => core.writer('default', (err, feed) => (feed1 = feed, cb())), - cb => core.writer('second', (err, feed) => (feed2 = feed, cb())), + cb => core.writer('default', (err, feed) => { + t.error(err) + feed1 = feed + cb() + }), + cb => core.writer('second', (err, feed) => { + t.error(err) + feed2 = feed + cb() + }), cb => feed1.append(1, cb), cb => feed1.append(1, cb), cb => feed2.append(3, cb), cb => { core.api.sum.get(function (err, value) { + t.error(err) t.equals(5, value) cb() }) } ]) - const core2 = kappa(ram, { valueEncoding: 'json' }) - t.end() }) @@ -49,10 +56,10 @@ function createSumView () { return sumview } -function replicate (a, b, opts, cb) { - if (typeof opts === 'function') return replicate(a, b, null, cb) - if (!opts) opts = { live: true } - const stream = a.replicate(opts) - stream.pipe(b.replicate(opts)).pipe(stream) - setImmediate(cb) -} +// function replicate (a, b, opts, cb) { +// if (typeof opts === 'function') return replicate(a, b, null, cb) +// if (!opts) opts = { live: true } +// const stream = a.replicate(opts) +// stream.pipe(b.replicate(opts)).pipe(stream) +// setImmediate(cb) +// } From 0046f80e4d632efc709f50201bdba6785c8c4715 Mon Sep 17 00:00:00 2001 From: "Franz Heinzmann (Frando)" Date: Thu, 24 Oct 2019 17:10:45 +0200 Subject: [PATCH 23/85] Make methods public --- kappa.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/kappa.js b/kappa.js index 3dfa75b..178eb7b 100644 --- a/kappa.js +++ b/kappa.js @@ -45,6 +45,7 @@ module.exports = class Kappa extends EventEmitter { if (view.api) { this.api[name] = { + name, ready (cb) { self.onViewIndexed(name, cb) } @@ -122,7 +123,7 @@ module.exports = class Kappa extends EventEmitter { } onViewIndexed (name, cb) { - const flows = this._flowsByView(name).filter(f => !f.parent) + const flows = this.flowsByView(name).filter(f => !f.parent) let pending = flows.length + 1 flows.forEach(flow => flow.ready(done)) done() @@ -158,7 +159,7 @@ module.exports = class Kappa extends EventEmitter { clear (viewName, cb) { const view = this.views[viewName] if (!view) throw new Error('Unknown view: ' + viewName) - const flows = this._flowsByView(viewName) + const flows = this.flowsByView(viewName) flows.forEach(flow => flow.pause()) let pending = flows.length + 1 @@ -174,11 +175,11 @@ module.exports = class Kappa extends EventEmitter { } } - _flowsByView (name) { + flowsByView (name) { return this.flows.filter(flow => flow.view.name === name) } - _flowsBySource (name) { + flowsBySource (name) { return this.flows.filter(flow => flow.source.name === name) } From d81e5eff60088fa919d7ec5ec1565ae49e7511bb Mon Sep 17 00:00:00 2001 From: "Franz Heinzmann (Frando)" Date: Sun, 27 Oct 2019 18:09:46 +0100 Subject: [PATCH 24/85] Cleanup open & ready Also removes a "parent flow" leftover of the removed subflows pattern. --- kappa.js | 37 ++++++++++++++++++++++++++----------- 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/kappa.js b/kappa.js index 178eb7b..77ae1ca 100644 --- a/kappa.js +++ b/kappa.js @@ -26,7 +26,7 @@ module.exports = class Kappa extends EventEmitter { this._flowsByName = {} this._states = {} this._status = Status.Ready - this.ready = thunky(this._ready.bind(this)) + this.open = thunky(this._open.bind(this)) } /** @@ -47,7 +47,7 @@ module.exports = class Kappa extends EventEmitter { this.api[name] = { name, ready (cb) { - self.onViewIndexed(name, cb) + self._onViewIndexed(name, cb) } } for (let [key, value] of Object.entries(view.api)) { @@ -115,24 +115,27 @@ module.exports = class Kappa extends EventEmitter { connectAll () { for (const source of Object.values(this.sources)) { - if (source.parent) continue for (const view of Object.values(this.views)) { this.connect(source.name, view.name) } } } - onViewIndexed (name, cb) { - const flows = this.flowsByView(name).filter(f => !f.parent) - let pending = flows.length + 1 - flows.forEach(flow => flow.ready(done)) - done() + ready (viewNames, cb) { + if (!this._opened) return this.open(this.ready.bind(this, viewNames, cb)) + if (typeof viewNames === 'function') return this.ready(null, viewNames) + if (typeof viewNames === 'string') viewNames = [viewNames] + if (!viewNames) viewNames = Object.keys(this.views) + + let pending = viewNames.length + viewNames.forEach(viewName => this._onViewIndexed(viewName, done)) function done () { if (--pending === 0) cb() } } - _ready (cb) { + _open (cb) { + const self = this if (this.opts.autoconnect) this.connectAll() let pending = this.flows.length + 1 @@ -140,7 +143,9 @@ module.exports = class Kappa extends EventEmitter { finish() function finish () { - if (--pending === 0) process.nextTick(cb) + if (--pending !== 0) return + self._opened = true + process.nextTick(cb) } } @@ -183,6 +188,16 @@ module.exports = class Kappa extends EventEmitter { return this.flows.filter(flow => flow.source.name === name) } + _onViewIndexed (name, cb) { + let pending = this.flows.length + 1 + this.flows.forEach(flow => flow.ready(done)) + done() + function done () { + if (--pending === 0) cb() + } + } + + _fetchState (flow, cb) { // if (flow.view.fetchState) return flow.view.fetchState(flow, cb) cb(null, this._states[flow.name]) @@ -208,7 +223,6 @@ class Flow extends EventEmitter { if (!this.source.name) this.source.name = opts.name this.name = this.source.name + '~' + this.view.name - this.parent = opts.parent this.status = Status.Ready this._opened = false @@ -269,6 +283,7 @@ class Flow extends EventEmitter { } _onbatch (msgs, cb) { + if (!msgs.length) cb(null, msgs) let steps = [ this.source.transform, this.view.filter, From 4f21708c0666d8b3622caf970bf0cc15bf295755 Mon Sep 17 00:00:00 2001 From: "Franz Heinzmann (Frando)" Date: Tue, 29 Oct 2019 12:24:29 +0100 Subject: [PATCH 25/85] Better multifeed backward compatibility --- index.js | 1 + package.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index 16a8682..f0efc22 100644 --- a/index.js +++ b/index.js @@ -17,6 +17,7 @@ function kappaClassic (storage, opts = {}) { kappa.writer = feeds.writer.bind(feeds) kappa.feed = feeds.feed.bind(feeds) kappa.replicate = feeds.replicate.bind(feeds) + kappa._logs = feeds return kappa } diff --git a/package.json b/package.json index 427290e..29e827f 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "kappa-core", "description": "Minimal peer-to-peer database, based on kappa architecture.", "author": "Stephen Whitmore ", - "version": "4.0.1", + "version": "5.0.0-alpha1", "repository": { "url": "git://github.com/noffle/kappa-core.git" }, From 6605098e892c924404cb5596082600a0eaec7e13 Mon Sep 17 00:00:00 2001 From: "Franz Heinzmann (Frando)" Date: Thu, 7 Nov 2019 11:15:56 +0100 Subject: [PATCH 26/85] docs --- README.md | 48 ++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 42 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 89d4a4a..5cd70c9 100644 --- a/README.md +++ b/README.md @@ -6,17 +6,53 @@ Make the base Kappa core dependencyless. See `./kappa.js`. `Kappa` just deals with *sources* and *views*. Sources pull values based on their last state, views process values in a map function. -`kappa.source(sourceName, createSource, opts)` registers a new source. `createSource` is invoked after the source is connected to a view and should return an object with at least a `pull` handler. +![kappa flow](kappa-graph.svg) -`kappa.use(viewName, view)` registers a new view (same as in kappa-core v4). +## API -`kappa.connect()` connects sourceName to a viewName, thus creating a *flow*. A flow can be started, stopped, restarted. The flow creates a source instance, handles state and pulls messages from the source instance. +`const { Kappa } = require('kappa-core')` -In `index.js` this new, dependencyless Kappa is wired to multifeed, creating an API compatible kappa-core. It passes `cabal-core` tests! And `multifeed-index` is not needed anymore even, just the two very simple functions in `sources/` (and the hypercoreSource can be used on its own also). +#### `const kappa = new Kappa(opts)` -More docs come soon. +Create a new kappa core. opts are: +* `autoconnect: boolean`: connect all sources to all views (default: true) +* `autostart: boolean`: start indexing immediately (default: true) -![kappa flow](kappa-graph.svg) + +#### `kappa.source(sourceName, createSource, opts)` + +Registers a new source. + +`createSource` is called with `(context, opts)` after the source is connected to a view. `context` is an object with: +* `view`: The view this source instance is connected to +* `onupdate`: A callback to signal to the kappa that this source has new data. This will trigger the `pull` function to be called (if processing is not paused). + +`createSource` has to return a source handler object with: + +* `pull: function (state, next)`: Pull new messages from the view. Should call `next(nextState, msgs, moreWork)` where `msgs` is an array of messages, `moreWork` is a boolean that indicates if `pull` should be called again right away. `state` and `nextState` are buffers (TODO: this is not enforced atm) that the source can use to track its indexing progress. If `state` is null, the indexing has to be restarted. If a source does its own proress tracking, it can ignore the state value (apart from null meaning rewind). + +#### `kappa.use(viewName, view)` + +Register a view. viewName is a string, + +* `viewName` (string) the name of the view, has to be unique per kappa core +* `view` object with properties `map (msgs, next) {}` (required) and optionally `filter (msgs, next) {}` + + +#### `kappa.connect(sourceName, viewName)` + +Create a source instance and connect it to a view. The combination of sourceInstance and view is called a *flow*. A flow can be started, stopped, restarted. The flow creates a source instance and pulls messages from the source instance. + +#### `kappa.clear(viewName)` + +#### `kappa.flowsBySource(sourceName)` + +#### `kappa.flowsByView(viewName)` + + +## kappa classic + +In `index.js` this new, dependencyless Kappa is wired to multifeed, creating an API compatible kappa-core. This is the default export at the moment. It passes `cabal-core` tests! And `multifeed-index` is not needed anymore even, just the two very simple functions in `sources/` (and the hypercoreSource can be used on its own also). --- From 30753f06c76368470c88be4b4c798d8a54d142dc Mon Sep 17 00:00:00 2001 From: "Franz Heinzmann (Frando)" Date: Thu, 7 Nov 2019 11:16:39 +0100 Subject: [PATCH 27/85] context for views --- kappa.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/kappa.js b/kappa.js index 77ae1ca..498b0b3 100644 --- a/kappa.js +++ b/kappa.js @@ -197,7 +197,6 @@ module.exports = class Kappa extends EventEmitter { } } - _fetchState (flow, cb) { // if (flow.view.fetchState) return flow.view.fetchState(flow, cb) cb(null, this._states[flow.name]) @@ -217,9 +216,12 @@ class Flow extends EventEmitter { this.view = view this.opts = opts - this.source = createSource({ - onupdate: this._onupdate.bind(this) - }, opts, this.kappa) + const context = { + onupdate: this._onupdate.bind(this), + view + } + + this.source = createSource(context, opts) if (!this.source.name) this.source.name = opts.name this.name = this.source.name + '~' + this.view.name From e8cc5b14c2407619239da98d9e4d8d5824eb8195 Mon Sep 17 00:00:00 2001 From: ameba23 Date: Thu, 7 Nov 2019 17:17:03 +0100 Subject: [PATCH 28/85] bump corestore to 4.0.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 29e827f..1816fe4 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ }, "devDependencies": { "bitfield-db": "^1.2.2", - "corestore": "2", + "corestore": "^4.0.0", "hypercore": "^8", "hyperdrive": "^10", "hyperdrive-schemas": "^0.12.0", From ed0f44a6f0789fa0dee6725f353ff7e291c43bd1 Mon Sep 17 00:00:00 2001 From: ameba23 Date: Thu, 7 Nov 2019 17:17:54 +0100 Subject: [PATCH 29/85] add corestore source --- sources/corestore.js | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 sources/corestore.js diff --git a/sources/corestore.js b/sources/corestore.js new file mode 100644 index 0000000..fde3ba1 --- /dev/null +++ b/sources/corestore.js @@ -0,0 +1,23 @@ +const hypercoreSource = require('./hypercore') +const multi = require('./util/multisource') + +module.exports = function corestoreSource (handlers, opts) { + const store = opts.store + const { pull, addSource } = multi(handlers) + + return { open, pull } + + function open (next) { + store.ready(() => { + store.list().forEach(feed => _onfeed(feed)) + store.on('feed', feed => _onfeed(feed)) + next() + }) + } + + function _onfeed (feed, cb) { + feed.ready(() => { + addSource(feed.key.toString('hex'), hypercoreSource, { feed }) + }) + } +} From 967a3a31455f6c391fdd9e25f8625c343cd99b16 Mon Sep 17 00:00:00 2001 From: ameba23 Date: Thu, 7 Nov 2019 17:18:17 +0100 Subject: [PATCH 30/85] add test for corestore source --- test/corestore.js | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 test/corestore.js diff --git a/test/corestore.js b/test/corestore.js new file mode 100644 index 0000000..bdbcdcd --- /dev/null +++ b/test/corestore.js @@ -0,0 +1,43 @@ +const tape = require('tape') +const { Kappa } = require('..') +const Corestore = require('corestore') +const ram = require('random-access-memory') +const corestoreSource = require('../sources/corestore') + +tape('corestore source', t => { + const kappa = new Kappa() + + const store = new Corestore(ram) + store.ready(() => { + const core1 = store.default() + + kappa.source('corestore', corestoreSource, { store }) + + const core2 = store.get() + + let res = [] + kappa.use('view', { + map (msgs, next) { + res = res.concat(msgs.map(msg => msg.value)) + next() + }, + api: { + collect (kappa, cb) { + this.ready(() => cb(null, res)) + } + } + }) + + core1.append(1) + core2.append(2) + core1.append(3) + + setImmediate(() => { + kappa.api.view.collect((err, res) => { + t.error(err) + t.deepEqual(res, [1, 2, 3]) + t.end() + }) + }) + }) +}) From 37cececfcbc3cfe3a9ab1ab2903e07737b86bd0d Mon Sep 17 00:00:00 2001 From: ameba23 Date: Thu, 7 Nov 2019 17:41:08 +0100 Subject: [PATCH 31/85] add default error handler for hypercore source --- sources/hypercore.js | 1 + 1 file changed, 1 insertion(+) diff --git a/sources/hypercore.js b/sources/hypercore.js index 861d318..69af60a 100644 --- a/sources/hypercore.js +++ b/sources/hypercore.js @@ -23,6 +23,7 @@ module.exports = function hypercoreSource (handlers, opts) { return next(at) } feed.getBatch(at, to, { wait: false }, (err, res) => { + handlers.onerror = handlers.onerror || function (err) { throw err } if (err) return handlers.onerror(err) res = res.map((node, i) => ({ key: feed.key.toString('hex'), From 8516f33ea12305070c7fd13c26f87fd89ae96a92 Mon Sep 17 00:00:00 2001 From: ameba23 Date: Thu, 7 Nov 2019 17:41:33 +0100 Subject: [PATCH 32/85] valueencoding = json in corestore test --- test/corestore.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/corestore.js b/test/corestore.js index bdbcdcd..d352aed 100644 --- a/test/corestore.js +++ b/test/corestore.js @@ -9,11 +9,11 @@ tape('corestore source', t => { const store = new Corestore(ram) store.ready(() => { - const core1 = store.default() + const core1 = store.default({ valueEncoding: 'json' }) kappa.source('corestore', corestoreSource, { store }) - const core2 = store.get() + const core2 = store.get({ valueEncoding: 'json' }) let res = [] kappa.use('view', { From f08b157866b99bf79f75be479815ca953c232a61 Mon Sep 17 00:00:00 2001 From: ameba23 Date: Thu, 7 Nov 2019 17:58:38 +0100 Subject: [PATCH 33/85] function for corestore constructor --- test/hyperdrive.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/hyperdrive.js b/test/hyperdrive.js index c41116e..7b16ff4 100644 --- a/test/hyperdrive.js +++ b/test/hyperdrive.js @@ -1,14 +1,17 @@ const tape = require('tape') const ram = require('random-access-memory') const hyperdrive = require('hyperdrive') -const corestore = require('corestore') +const Corestore = require('corestore') const { runAll, replicate } = require('./lib/util') const { Kappa } = require('..') const hyperdriveSource = require('../sources/hyperdrive') +function corestore (storage) { return new Corestore(storage) } + tape('hyperdrive source', async t => { const cstore = corestore(ram) + var drive1, drive2, drive3, driveB, kappa, kappaB const expected = [ From d075ab4ca65c1cbd9c8a16437ba1dfd46e113a73 Mon Sep 17 00:00:00 2001 From: "Franz Heinzmann (Frando)" Date: Thu, 14 Nov 2019 17:59:13 +0100 Subject: [PATCH 34/85] Add transform opt --- kappa-old.js | 129 --------------------------------------------------- kappa.js | 1 + 2 files changed, 1 insertion(+), 129 deletions(-) delete mode 100644 kappa-old.js diff --git a/kappa-old.js b/kappa-old.js deleted file mode 100644 index 4c95c4f..0000000 --- a/kappa-old.js +++ /dev/null @@ -1,129 +0,0 @@ -var inherits = require('inherits') -var EventEmitter = require('events').EventEmitter -var hypercore = require('hypercore') -var multifeed = require('multifeed') -var indexer = require('multifeed-index') - -module.exports = Kappa - -function Kappa (storage, opts) { - if (!(this instanceof Kappa)) return new Kappa(storage, opts) - if (!opts) opts = {} - - this._logs = opts.multifeed || multifeed(hypercore, storage, opts) - this._indexes = {} - - this.api = {} -} - -inherits(Kappa, EventEmitter) - -Kappa.prototype.use = function (name, version, view) { - var self = this - if (typeof version !== 'number') { - view = version - version = undefined - } - var idx = indexer(Object.assign({}, view, { - log: this._logs, - version: version, - maxBatch: view.maxBatch || 10, - batch: view.map - })) - idx.on('error', function (err) { - self.emit('error', err) - }) - if (view.indexed) idx.on('indexed', view.indexed) - this._indexes[name] = idx - this.api[name] = {} - this.api[name].ready = idx.ready.bind(idx) - for (var key in view.api) { - if (typeof view.api[key] === 'function') this.api[name][key] = view.api[key].bind(idx, this) - else this.api[name][key] = view.api[key] - } -} - -Kappa.prototype.feeds = function () { - return this._logs.feeds() -} - -Kappa.prototype.ready = function (viewNames, cb) { - if (typeof viewNames === 'function') { - cb = viewNames - viewNames = [] - } - - if (typeof viewNames === 'string') viewNames = [viewNames] - if (viewNames.length === 0) { - viewNames = Object.keys(this._indexes) - } - - var pending = viewNames.length + 1 - var self = this - this._logs.ready(function () { - for (var i = 0; i < viewNames.length; i++) { - self._indexes[viewNames[i]].ready(done) - } - done() - }) - - function done () { - if (!--pending) cb() - } -} - -Kappa.prototype.pause = function (viewNames, cb) { - if (typeof viewNames === 'function') { - cb = viewNames - viewNames = [] - } - cb = cb || noop - - if (!viewNames) viewNames = [] - if (typeof viewNames === 'string') viewNames = [viewNames] - if (viewNames.length === 0) { - viewNames = Object.keys(this._indexes) - } - - var pending = viewNames.length + 1 - var self = this - this._logs.ready(function () { - for (var i = 0; i < viewNames.length; i++) { - self._indexes[viewNames[i]].pause(done) - } - done() - }) - - function done () { - if (!--pending) cb() - } -} - -Kappa.prototype.resume = function (viewNames) { - if (!viewNames) viewNames = [] - if (typeof viewNames === 'string') viewNames = [viewNames] - if (viewNames.length === 0) { - viewNames = Object.keys(this._indexes) - } - - var self = this - this._logs.ready(function () { - for (var i = 0; i < viewNames.length; i++) { - self._indexes[viewNames[i]].resume() - } - }) -} - -Kappa.prototype.writer = function (name, cb) { - this._logs.writer(name, cb) -} - -Kappa.prototype.feed = function (key) { - return this._logs.feed(key) -} - -Kappa.prototype.replicate = function (opts) { - return this._logs.replicate(opts) -} - -function noop () {} diff --git a/kappa.js b/kappa.js index 498b0b3..c62135b 100644 --- a/kappa.js +++ b/kappa.js @@ -288,6 +288,7 @@ class Flow extends EventEmitter { if (!msgs.length) cb(null, msgs) let steps = [ this.source.transform, + this.opts.transform, this.view.filter, this.view.transform ].filter(f => f) From 53e908ade3707486412cde1c2cb128af5db5ae55 Mon Sep 17 00:00:00 2001 From: "Franz Heinzmann (Frando)" Date: Fri, 15 Nov 2019 01:26:46 +0100 Subject: [PATCH 35/85] Add stacked views. --- kappa.js | 133 +++++++++++++++++++++++++++---------- test/stacked-view.js | 151 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 251 insertions(+), 33 deletions(-) create mode 100644 test/stacked-view.js diff --git a/kappa.js b/kappa.js index c62135b..ff976f3 100644 --- a/kappa.js +++ b/kappa.js @@ -39,21 +39,11 @@ module.exports = class Kappa extends EventEmitter { * @param {function(msgs, next)} [view.clearIndex] clear index */ use (name, view) { - const self = this view.name = name this.views[name] = view if (view.api) { - this.api[name] = { - name, - ready (cb) { - self._onViewIndexed(name, cb) - } - } - for (let [key, value] of Object.entries(view.api)) { - if (typeof value === 'function') value = value.bind(this.api[name], this) - this.api[name][key] = value - } + this._assignApi(name, view.api) } if (this.opts.autoconnect) { @@ -64,6 +54,23 @@ module.exports = class Kappa extends EventEmitter { this.emit('view', view.name, view) } + /** + * Register a stack of views in the kappa. + * + * All views will run together and in the order as passed in. + * If the view has an API, set its name property. + * + * @param {string} name a unique name for this stack + * @param {Array} views an array of view handler objects + */ + useStack (name, views) { + views.forEach(view => { + if (view.name) this._assignApi(view.name, view.api) + }) + + this.use(name, new StackedView(views)) + } + /** * Register a source in the kappa. * @@ -127,11 +134,14 @@ module.exports = class Kappa extends EventEmitter { if (typeof viewNames === 'string') viewNames = [viewNames] if (!viewNames) viewNames = Object.keys(this.views) - let pending = viewNames.length - viewNames.forEach(viewName => this._onViewIndexed(viewName, done)) - function done () { - if (--pending === 0) cb() - } + // wait a tick + process.nextTick(() => { + let pending = viewNames.length + viewNames.forEach(viewName => this._onViewIndexed(viewName, done)) + function done () { + if (--pending === 0) cb() + } + }) } _open (cb) { @@ -181,7 +191,11 @@ module.exports = class Kappa extends EventEmitter { } flowsByView (name) { - return this.flows.filter(flow => flow.view.name === name) + return this.flows.filter(flow => { + if (flow.view.name === name) return true + if (flow.view.views) return !!flow.view.views.find(view => view.name === name) + return false + }) } flowsBySource (name) { @@ -189,7 +203,8 @@ module.exports = class Kappa extends EventEmitter { } _onViewIndexed (name, cb) { - let pending = this.flows.length + 1 + const flows = this.flowsByView(name) + let pending = flows.length + 1 this.flows.forEach(flow => flow.ready(done)) done() function done () { @@ -197,6 +212,21 @@ module.exports = class Kappa extends EventEmitter { } } + _assignApi (name, api = {}) { + const self = this + this.api[name] = { + name, + ready (cb) { + self._onViewIndexed(name, cb) + } + } + const context = this.opts.context || this + for (let [key, value] of Object.entries(api)) { + if (typeof value === 'function') value = value.bind(this.api[name], context) + this.api[name][key] = value + } + } + _fetchState (flow, cb) { // if (flow.view.fetchState) return flow.view.fetchState(flow, cb) cb(null, this._states[flow.name]) @@ -209,6 +239,32 @@ module.exports = class Kappa extends EventEmitter { } } +class StackedView { + constructor (views) { + this.views = views + } + + open (cb) { + const self = this + const views = self.views.filter(v => v.open) + next() + function next (idx = 0) { + if (idx === views.length) return cb() + if (views[idx].open) views[idx].open(() => process.nextTick(next, idx + 1)) + else next(idx + 1) + } + } + + map (msgs, cb) { + const self = this + next() + function next (idx = 0) { + if (idx === self.views.length) return cb() + applyView(self.views[idx], msgs, () => process.nextTick(next, idx + 1)) + } + } +} + class Flow extends EventEmitter { constructor (kappa, view, createSource, opts) { super() @@ -235,13 +291,9 @@ class Flow extends EventEmitter { if (this._opened) return cb() const self = this - let pending = 1 - if (this.view.open) ++pending && this.view.open(finish) - if (this.source.open) ++pending && this.source.open(finish) - finish() + awaitAll([this.view, this.source], 'open', finish) function finish () { - if (--pending !== 0) return self._opened = true self._run() cb() @@ -286,15 +338,9 @@ class Flow extends EventEmitter { _onbatch (msgs, cb) { if (!msgs.length) cb(null, msgs) - let steps = [ - this.source.transform, - this.opts.transform, - this.view.filter, - this.view.transform - ].filter(f => f) - runAll(msgs, steps, msgs => { - if (!msgs.length) cb(null, msgs) - else this.view.map(msgs, () => cb(null, msgs)) + let prepare = [this.source.transform, this.opts.transform] + runThrough(msgs, prepare, msgs => { + applyView(this.view, msgs, () => cb(null, msgs)) }) } @@ -338,7 +384,16 @@ class Flow extends EventEmitter { } } -function runAll (state, fns, final) { +function applyView (view, msgs, cb) { + const prepare = [view.filter, view.transform] + runThrough(msgs, prepare, (msgs) => { + if (!msgs.length) return cb() + view.map(msgs, cb) + }) +} + +function runThrough (state, fns, final) { + fns = fns.filter(f => f) next(state) function next (state) { const fn = fns.shift() @@ -349,4 +404,16 @@ function runAll (state, fns, final) { } } +function awaitAll (objs, fn, args, cb) { + if (typeof args === 'function') return awaitAll(objs, fn, [], args) + objs = objs.filter(obj => obj[fn]) + let pending = objs.length + if (!pending) return cb() + args.push(done) + objs.forEach(obj => obj[fn](...args)) + function done () { + if (--pending === 0) cb() + } +} + function noop () {} diff --git a/test/stacked-view.js b/test/stacked-view.js new file mode 100644 index 0000000..5add3ae --- /dev/null +++ b/test/stacked-view.js @@ -0,0 +1,151 @@ +const tape = require('tape') +const { Kappa } = require('..') +const hypercore = require('hypercore') +const ram = require('random-access-memory') +const hypercoreSource = require('../sources/hypercore') + +tape('stacked views run in order', t => { + const kappa = new Kappa() + const core = hypercore(ram, { valueEncoding: 'json' }) + kappa.source('core', hypercoreSource, { feed: core }) + + let first = 0 + let second = 0 + let firstOpn, secondOpn + const stack = [ + { + name: 'first', + open (cb) { + t.equal(secondOpn, undefined, 'first opens first') + firstOpn = true + cb() + }, + map (msgs, next) { + kappa.api.second.count((count) => { + t.equal(count, first, 'first ran first') + first = first + msgs.length + next() + }) + }, + api: { + count (kappa, cb) { process.nextTick(cb, first) } + } + }, + { + name: 'second', + open (cb) { + t.equal(firstOpn, true, 'second opens last') + secondOpn = true + cb() + }, + map (msgs, next) { + kappa.api.first.count((count) => { + t.equal(count, second + msgs.length, 'second ran second') + second = second + msgs.length + next() + }) + }, + api: { + count (kappa, cb) { process.nextTick(cb, second) } + } + } + ] + + kappa.useStack('stack', stack) + + core.append([1, 2, 3]) + setTimeout(() => { + core.append([1, 2, 3]) + kappa.ready(() => { + t.equal(first, 6) + t.equal(second, 6) + t.end() + }) + }, 10) +}) + +tape('stacked view linked kv', t => { + const kappa = new Kappa() + const core = hypercore(ram, { valueEncoding: 'json' }) + kappa.source('core', hypercoreSource, { feed: core }) + const core2 = hypercore(ram, { valueEncoding: 'json' }) + kappa.source('core2', hypercoreSource, { feed: core2 }) + + const links = {} + const keys = {} + const ops = [] + kappa.useStack('stack', [ + { + name: 'links', + map (msgs, next) { + msgs.forEach(msg => { + const id = toId(msg) + if (msg.value.links) msg.value.links.forEach(l => (links[l] = true)) + const currentLinks = kappa.api.links.get(msg.value.key) + keys[msg.value.key] = [...currentLinks, id] + }) + next() + }, + api: { + isLinked (kappa, msg) { + return !!links[toId(msg)] + }, + get (kappa, key) { + return keys[key] || [] + } + } + }, + { + name: 'ops', + map (msgs, next) { + msgs.forEach(msg => { + let op + if (kappa.api.links.isLinked(msg)) { + op = 'outdated' + } else if (kappa.api.links.get(msg.value.key).length > 1) { + op = 'update' + } else { + op = 'create' + } + ops.push({ op, value: msg.value, id: toId(msg), idx: ops.length }) + }) + next() + }, + api: { + log (kappa) { + return ops + } + } + } + ]) + + readyAll([core, core2], () => { + core.append({ key: 'earth', value: 'best' }) + core.append({ key: 'mars', value: 'red', links: [core2.key.toString('hex') + '@0'] }) + core.append({ key: 'earth', value: 'burned', links: [core.key.toString('hex') + '@0'] }) + + setTimeout(() => { + core2.append({ key: 'mars', value: 'hotter' }) + core2.append({ key: 'earth', value: 'fuuuu' }) + }, 0) + + setTimeout(() => { + kappa.ready('ops', () => { + let log = kappa.api.ops.log() + t.equal(log.filter(l => l.op === 'create').length, 2, 'two creations') + t.equal(log.filter(l => l.op === 'update').length, 2, 'two updates') + t.equal(log.filter(l => l.op === 'outdated').length, 1, 'one outdated') + t.end() + }) + }, 10) + }) +}) + +function toId (msg) { + return msg.key + '@' + msg.seq +} + +function readyAll (cores, cb) { + let pending = cores.length + cores.forEach(core => core.ready(() => (--pending === 0 && cb()))) +} From 6a3ff630eadfc150f68217945ed1e436597fdd28 Mon Sep 17 00:00:00 2001 From: "Franz Heinzmann (Frando)" Date: Fri, 15 Nov 2019 02:30:13 +0100 Subject: [PATCH 36/85] clearIndex for stacked views --- kappa.js | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/kappa.js b/kappa.js index ff976f3..938f50c 100644 --- a/kappa.js +++ b/kappa.js @@ -246,11 +246,10 @@ class StackedView { open (cb) { const self = this - const views = self.views.filter(v => v.open) next() function next (idx = 0) { - if (idx === views.length) return cb() - if (views[idx].open) views[idx].open(() => process.nextTick(next, idx + 1)) + if (idx === self.views.length) return cb() + if (self.views[idx].open) self.views[idx].open(() => process.nextTick(next, idx + 1)) else next(idx + 1) } } @@ -263,6 +262,19 @@ class StackedView { applyView(self.views[idx], msgs, () => process.nextTick(next, idx + 1)) } } + + clearIndex (cb) { + let pending = this.views.length + this.views.views.forEach(view => view.clearIndex ? view.clearIndex(done) : done()) + function done () { --pending === 0 && cb() } + } + + get api () { + return this.views.reduce((agg, view) => { + agg[view.name] = view.api + return agg + }, {}) + } } class Flow extends EventEmitter { From c50d72deeed4459eb3b5d3e276af62c89ae83be2 Mon Sep 17 00:00:00 2001 From: "Franz Heinzmann (Frando)" Date: Thu, 5 Dec 2019 11:23:15 +0100 Subject: [PATCH 37/85] Rework API after discussions and feedback. A kappa now is a list of flows where each connects a single source to a single view. Hyperdrive source and stacked views still needs to be updated. Other tests pass. --- README.md | 30 +-- index.js | 21 +- kappa.js | 368 ++++++++++-------------------------- package.json | 5 +- sources/corestore.js | 39 ++-- sources/hypercore.js | 63 ++++-- sources/hyperdrive.js | 20 +- sources/multifeed.js | 23 ++- sources/util/merge-pull.js | 33 ++++ sources/util/multisource.js | 43 ----- sources/util/state.js | 65 +++++++ test/basic.js | 67 ++++--- test/corestore.js | 7 +- test/hypercore.js | 7 +- test/hyperdrive.js | 6 +- test/multifeed.js | 21 +- test/stacked-view.js | 5 +- 17 files changed, 383 insertions(+), 440 deletions(-) create mode 100644 sources/util/merge-pull.js delete mode 100644 sources/util/multisource.js create mode 100644 sources/util/state.js diff --git a/README.md b/README.md index 5cd70c9..892babf 100644 --- a/README.md +++ b/README.md @@ -29,25 +29,33 @@ Registers a new source. `createSource` has to return a source handler object with: -* `pull: function (state, next)`: Pull new messages from the view. Should call `next(nextState, msgs, moreWork)` where `msgs` is an array of messages, `moreWork` is a boolean that indicates if `pull` should be called again right away. `state` and `nextState` are buffers (TODO: this is not enforced atm) that the source can use to track its indexing progress. If `state` is null, the indexing has to be restarted. If a source does its own proress tracking, it can ignore the state value (apart from null meaning rewind). -#### `kappa.use(viewName, view)` +#### `kappa.use(name, source, view)` Register a view. viewName is a string, * `viewName` (string) the name of the view, has to be unique per kappa core -* `view` object with properties `map (msgs, next) {}` (required) and optionally `filter (msgs, next) {}` - - -#### `kappa.connect(sourceName, viewName)` - -Create a source instance and connect it to a view. The combination of sourceInstance and view is called a *flow*. A flow can be started, stopped, restarted. The flow creates a source instance and pulls messages from the source instance. +* `source` object with properties + * `open: function (flow, next)` (optional) + * `pull: function (next)`: Pull new messages from the view. Should call `next` with either nothing or an object that looks like this: + ```javascript + { + messages: [], // array of messages, + finished: true, // if set to false, signal that more messages are pending + onindexed: function (cb) {} // will be called when the view finished indexing + } + ``` + The source has to track its state, so that subsequent calls to `pull()` do not return the same messages. + + There are several source handlers included in kappa-core (TODO: document sources). See the tests and sources directories. + * `reset: function (cb) {}`: Reset internal state and restart indexing. +* `view` object with properties + * `open: function (flow, next)` (optional) + * `map: function (msgs, next)` (required) + * `filter: function (msgs, next)` (optional), #### `kappa.clear(viewName)` -#### `kappa.flowsBySource(sourceName)` - -#### `kappa.flowsByView(viewName)` ## kappa classic diff --git a/index.js b/index.js index f0efc22..95f6b4a 100644 --- a/index.js +++ b/index.js @@ -1,23 +1,4 @@ const Kappa = require('./kappa') -module.exports = kappaClassic +module.exports = Kappa module.exports.Kappa = Kappa - -function kappaClassic (storage, opts = {}) { - const multifeed = require('multifeed') - const hypercore = require('hypercore') - const multifeedSource = require('./sources/multifeed') - - const feeds = opts.multifeed || multifeed(hypercore, storage, opts) - - const kappa = new Kappa() - - kappa.source('multifeed', multifeedSource, { feeds }) - - kappa.writer = feeds.writer.bind(feeds) - kappa.feed = feeds.feed.bind(feeds) - kappa.replicate = feeds.replicate.bind(feeds) - kappa._logs = feeds - - return kappa -} diff --git a/kappa.js b/kappa.js index 938f50c..a267d19 100644 --- a/kappa.js +++ b/kappa.js @@ -15,124 +15,60 @@ module.exports = class Kappa extends EventEmitter { constructor (opts = {}) { super() this.opts = { - autoconnect: true, autostart: true, ...opts } - this.views = {} - this.sources = {} this.api = {} - this.flows = [] - this._flowsByName = {} - this._states = {} - this._status = Status.Ready + this.flows = {} this.open = thunky(this._open.bind(this)) } - /** - * Register a view in the kappa. - * - * @param {string} name a unique name for this view - * @param {object} view handlers object - * @param {function(msgs, next)} view.map process messages - * @param {function(msgs, next)} [view.filter] filter messages before map - * @param {function(msgs, next)} [view.clearIndex] clear index - */ - use (name, view) { - view.name = name - this.views[name] = view - - if (view.api) { - this._assignApi(name, view.api) - } - - if (this.opts.autoconnect) { - Object.keys(this.sources).forEach(sourceName => this.connect(sourceName, name)) - this.on('source', sourceName => this.connect(sourceName, name)) - } - - this.emit('view', view.name, view) + use (name, source, view, opts = {}) { + opts.name = name + const flow = new Flow(this, { source, view, opts, status: this.status }) + this.flows[name] = flow + this._assignApi(name, flow) + if (this.opts.autostart) flow.open() + this.emit('view', name) + return flow } - /** - * Register a stack of views in the kappa. - * - * All views will run together and in the order as passed in. - * If the view has an API, set its name property. - * - * @param {string} name a unique name for this stack - * @param {Array} views an array of view handler objects - */ - useStack (name, views) { - views.forEach(view => { - if (view.name) this._assignApi(view.name, view.api) + _open (cb) { + const flows = Object.values(this.flows) + let pending = flows.length + Object.values(flows).forEach(flow => { + flow.open(() => { + if (--pending === 0) cb() + }) }) - - this.use(name, new StackedView(views)) } - /** - * Register a source in the kappa. - * - * @param {string} name a unique name for this source - * @param {function} createSource source constructor - * @param {opts} opts - */ - source (name, createSource, opts = {}) { - if (this.sources[name]) return - opts.name = name - this.sources[name] = { name, createSource, opts } - - if (this.opts.autoconnect) { - Object.keys(this.views).forEach(viewName => this.connect(name, viewName)) - this.on('view', viewName => this.connect(name, viewName)) - } - - this.emit('source', name) + pause () { + this.status = Status.Paused + const flows = Object.values(this.flows) + flows.forEach(flow => flow.pause()) } - /** - * Connect a source to a view. Creates a new flow. - * - * @param {string} sourceName - * @param {string} viewName - * @return {object} flow - */ - connect (sourceName, viewName) { - if (!this.views[viewName]) throw new Error('Unknown view: ' + viewName) - if (!this.sources[sourceName]) throw new Error('Unknown source: ' + sourceName) - - const flowName = sourceName + '~' + viewName - if (this._flowsByName[flowName]) return this._flowsByName[flowName] - - const view = this.views[viewName] - const { createSource, opts } = this.sources[sourceName] - - const flow = new Flow(this, view, createSource, opts) - - this.flows.push(flow) - this._flowsByName[flow.name] = flow - - if (this.opts.autostart) flow.open() - - this.emit('connect', sourceName, viewName) - - return flow + resume () { + const flows = Object.values(this.flows) + if (this.status === Status.Paused) { + flows.forEach(flow => { + flow.resume() + }) + this.status = Status.Ready + } } - connectAll () { - for (const source of Object.values(this.sources)) { - for (const view of Object.values(this.views)) { - this.connect(source.name, view.name) - } - } + reset (name, cb) { + const flow = this.flows[name] + if (!flow) return cb(new Error('Unknown flow: ' + name)) + flow.reset(cb) } ready (viewNames, cb) { - if (!this._opened) return this.open(this.ready.bind(this, viewNames, cb)) if (typeof viewNames === 'function') return this.ready(null, viewNames) if (typeof viewNames === 'string') viewNames = [viewNames] - if (!viewNames) viewNames = Object.keys(this.views) + if (!viewNames) viewNames = Object.keys(this.flows) // wait a tick process.nextTick(() => { @@ -144,168 +80,66 @@ module.exports = class Kappa extends EventEmitter { }) } - _open (cb) { - const self = this - if (this.opts.autoconnect) this.connectAll() - - let pending = this.flows.length + 1 - this.flows.forEach(flow => flow.open(finish)) - finish() - - function finish () { - if (--pending !== 0) return - self._opened = true - process.nextTick(cb) - } - } - - pause (cb) { - this.status = Status.Paused - this.flows.forEach(flow => flow.pause()) - } - - resume (cb) { - if (this.status === Status.Paused) { - this.flows.forEach(flow => flow.resume()) - this.status = Status.Ready - } - } - - clear (viewName, cb) { - const view = this.views[viewName] - if (!view) throw new Error('Unknown view: ' + viewName) - const flows = this.flowsByView(viewName) - - flows.forEach(flow => flow.pause()) - let pending = flows.length + 1 - if (view.clearIndex) view.clearIndex(restartFlows) - else restartFlows() - - function restartFlows () { - flows.forEach(flow => flow.restart(finish)) - finish() - } - function finish () { - if (--pending === 0 && cb) cb() - } - } - - flowsByView (name) { - return this.flows.filter(flow => { - if (flow.view.name === name) return true - if (flow.view.views) return !!flow.view.views.find(view => view.name === name) - return false - }) - } - - flowsBySource (name) { - return this.flows.filter(flow => flow.source.name === name) - } - _onViewIndexed (name, cb) { - const flows = this.flowsByView(name) - let pending = flows.length + 1 - this.flows.forEach(flow => flow.ready(done)) - done() - function done () { - if (--pending === 0) cb() - } + const flow = this.flows[name] + if (!flow) return cb(new Error('Unknown view: ' + name)) + flow.ready(cb) } - _assignApi (name, api = {}) { - const self = this + _assignApi (name, flow) { this.api[name] = { name, ready (cb) { - self._onViewIndexed(name, cb) + flow.ready(cb) } } const context = this.opts.context || this - for (let [key, value] of Object.entries(api)) { - if (typeof value === 'function') value = value.bind(this.api[name], context) - this.api[name][key] = value - } - } - - _fetchState (flow, cb) { - // if (flow.view.fetchState) return flow.view.fetchState(flow, cb) - cb(null, this._states[flow.name]) - } - - _storeState (flow, state, cb) { - // if (flow.view.storeState) return flow.view.storeState(flow, state, cb) - this._states[flow.name] = state - cb() - } -} - -class StackedView { - constructor (views) { - this.views = views - } - - open (cb) { - const self = this - next() - function next (idx = 0) { - if (idx === self.views.length) return cb() - if (self.views[idx].open) self.views[idx].open(() => process.nextTick(next, idx + 1)) - else next(idx + 1) + if (flow.view.api) { + for (let [key, value] of Object.entries(flow.view.api)) { + if (typeof value === 'function') value = value.bind(this.api[name], context) + this.api[name][key] = value + } } - } - map (msgs, cb) { - const self = this - next() - function next (idx = 0) { - if (idx === self.views.length) return cb() - applyView(self.views[idx], msgs, () => process.nextTick(next, idx + 1)) + if (flow.source.api) { + this.api[name].source = {} + for (let [key, value] of Object.entries(flow.source.api)) { + if (typeof value === 'function') value = value.bind(this.api[name], context) + this.api[name].source[key] = value + } } } - - clearIndex (cb) { - let pending = this.views.length - this.views.views.forEach(view => view.clearIndex ? view.clearIndex(done) : done()) - function done () { --pending === 0 && cb() } - } - - get api () { - return this.views.reduce((agg, view) => { - agg[view.name] = view.api - return agg - }, {}) - } } class Flow extends EventEmitter { - constructor (kappa, view, createSource, opts) { + constructor (kappa, { source, view, opts = {}, status }) { super() + + this.opts = opts this.kappa = kappa + this.name = opts.name this.view = view - this.opts = opts - - const context = { - onupdate: this._onupdate.bind(this), - view - } - - this.source = createSource(context, opts) - if (!this.source.name) this.source.name = opts.name + this.source = source - this.name = this.source.name + '~' + this.view.name - - this.status = Status.Ready + this.status = status || Status.Ready this._opened = false this.open = thunky(this._open.bind(this)) } + get version () { + return this.view.version + } + _open (cb = noop) { if (this._opened) return cb() const self = this + let pending = 1 + if (this.view.open) ++pending && this.view.open(this, done) + if (this.source.open) ++pending && this.source.open(this, done) + done() - awaitAll([this.view, this.source], 'open', finish) - - function finish () { + function done () { + if (--pending !== 0) return self._opened = true self._run() cb() @@ -332,24 +166,29 @@ class Flow extends EventEmitter { else this._run() } - restart (cb = noop) { + reset (cb = noop) { + const self = this this.pause() process.nextTick(() => { - this.kappa._storeState(this, null, () => { - this.resume() + if (this.view.clearIndex) this.view.clearIndex(reset) + else reset() + }) + function reset () { + self.source.reset(() => { + self.resume() cb() }) - }) + } } - _onupdate () { + update () { if (!this._opened) return this.incomingUpdate = true process.nextTick(this._run.bind(this)) } _onbatch (msgs, cb) { - if (!msgs.length) cb(null, msgs) + if (!msgs.length) return cb(null, msgs) let prepare = [this.source.transform, this.opts.transform] runThrough(msgs, prepare, msgs => { applyView(this.view, msgs, () => cb(null, msgs)) @@ -364,29 +203,32 @@ class Flow extends EventEmitter { this.status = Status.Running - this.kappa._fetchState(this, (err, state) => { - if (err) return close(err) - this.source.pull(state, onbatch) - }) + this.source.pull(onbatch) - function onbatch (nextState, msgs, workMore) { - msgs = msgs || [] + function onbatch (result) { + if (!result) return close() if (self.status === Status.Paused) return close() - self._onbatch(msgs, (err, msgs) => { + const { messages = [], finished, onindexed } = result + self._onbatch(messages, (err, messages) => { if (err) return close(err) - self.kappa._storeState(self, nextState, err => { - close(err, msgs, workMore) - }) + close(null, { messages, finished, onindexed }) }) } - function close (err, msgs, workMore) { - if (err) self.kappa.emit(err, this) - else if (msgs.length && self.view.indexed) { - self.view.indexed(msgs) + function close (err, result) { + if (err) self.kappa.emit('error', err, this) + if (!result) return finish(true) + const { messages, finished, onindexed } = result + if (messages.length && self.view.indexed) { + self.view.indexed(messages) } + if (onindexed) onindexed(() => finish(finished)) + else finish(finished) + } + + function finish (finished) { self.status = Status.Ready - if (self.incomingUpdate || workMore) { + if (self.incomingUpdate || !finished) { self.incomingUpdate = false process.nextTick(self._run.bind(self)) } else { @@ -416,16 +258,16 @@ function runThrough (state, fns, final) { } } -function awaitAll (objs, fn, args, cb) { - if (typeof args === 'function') return awaitAll(objs, fn, [], args) - objs = objs.filter(obj => obj[fn]) - let pending = objs.length - if (!pending) return cb() - args.push(done) - objs.forEach(obj => obj[fn](...args)) - function done () { - if (--pending === 0) cb() - } -} +// function awaitAll (objs, fn, args, cb) { +// if (typeof args === 'function') return awaitAll(objs, fn, [], args) +// objs = objs.filter(obj => obj[fn]) +// let pending = objs.length +// if (!pending) return cb() +// args.push(done) +// objs.forEach(obj => obj[fn](...args)) +// function done () { +// if (--pending === 0) cb() +// } +// } function noop () {} diff --git a/package.json b/package.json index 1816fe4..2657134 100644 --- a/package.json +++ b/package.json @@ -16,16 +16,15 @@ "keywords": [], "dependencies": { "inherits": "^2.0.4", - "multifeed": "^4.0.0", - "multifeed-index": "^3.3.2", "stream-collector": "^1.0.1", "thunky": "^1.1.0" }, "devDependencies": { + "multifeed": "^5", "bitfield-db": "^1.2.2", "corestore": "^4.0.0", "hypercore": "^8", - "hyperdrive": "^10", + "hyperdrive": "github:mafintosh/hyperdrive#hypercore-8", "hyperdrive-schemas": "^0.12.0", "nanoiterator": "^1.2.0", "random-access-memory": "^3.1.1", diff --git a/sources/corestore.js b/sources/corestore.js index fde3ba1..e4025c7 100644 --- a/sources/corestore.js +++ b/sources/corestore.js @@ -1,23 +1,36 @@ const hypercoreSource = require('./hypercore') -const multi = require('./util/multisource') +const mergePull = require('./util/merge-pull') -module.exports = function corestoreSource (handlers, opts) { +module.exports = function corestoreSource (opts) { const store = opts.store - const { pull, addSource } = multi(handlers) - - return { open, pull } - - function open (next) { - store.ready(() => { - store.list().forEach(feed => _onfeed(feed)) - store.on('feed', feed => _onfeed(feed)) - next() - }) + const sources = [] + let flow = null + return { + open (_flow, cb) { + flow = _flow + store.ready(() => { + store.list().forEach(feed => _onfeed(feed)) + store.on('feed', feed => _onfeed(feed)) + cb() + }) + }, + pull (next) { + mergePull(sources, next) + } } function _onfeed (feed, cb) { feed.ready(() => { - addSource(feed.key.toString('hex'), hypercoreSource, { feed }) + const source = hypercoreSource({ + feed, + box: opts.box, + prefix: (opts.prefix || '') + flow.name + '!' + feed.key.toString('hex') + }) + sources.push(source) + source.open(flow, () => { + flow.update() + if (cb) cb() + }) }) } } diff --git a/sources/hypercore.js b/sources/hypercore.js index 69af60a..c191749 100644 --- a/sources/hypercore.js +++ b/sources/hypercore.js @@ -1,36 +1,61 @@ -module.exports = function hypercoreSource (handlers, opts) { - const maxBatch = opts.maxBatch || 50 - const feed = opts.feed +const State = require('./util/state') - // TODO: Maybe to this in the flow class, passing on opts? - const transform = opts.transform +module.exports = (...args) => new HypercoreSource(...args) - return { open, pull, transform } +class HypercoreSource { + constructor (opts = {}) { + this.opts = opts + this.feed = opts.feed + this.maxBatch = opts.maxBatch || 50 + } - function open (next) { - feed.ready(function () { - feed.on('append', handlers.onupdate) - feed.on('download', handlers.onupdate) - next() + open (flow, cb) { + this.flow = flow + this.state = new State({ + prefix: flow.name, + box: this.opts.box }) + this.feed.on('append', () => flow.update()) + this.feed.on('download', () => flow.update()) + this.flow.update() + this.state.putVersion(flow.version, cb) } - function pull (state, next) { - const at = state || 0 - const to = Math.min(feed.length, at + maxBatch) - if (!(to > at)) return next(at) + pull (cb) { + this.state.get((err, seq) => { + if (err) return cb(err) + return this._pull(seq, cb) + }) + } + + _pull (at, next) { + const self = this + const feed = this.feed + const len = feed.length + const to = Math.min(len, at + this.maxBatch) + + if (!(to > at)) return next() + if (!feed.has(at, to)) { - return next(at) + return next({ finished: true }) } + feed.getBatch(at, to, { wait: false }, (err, res) => { - handlers.onerror = handlers.onerror || function (err) { throw err } - if (err) return handlers.onerror(err) + if (err) return next() + res = res.map((node, i) => ({ key: feed.key.toString('hex'), seq: at + i, value: node })) - next(to, res, to < feed.length) + + next({ + messages: res, + finished: to === len, + onindexed (cb) { + self.state.put(to, cb) + } + }) }) } } diff --git a/sources/hyperdrive.js b/sources/hyperdrive.js index 4a3ecd3..f9cd33e 100644 --- a/sources/hyperdrive.js +++ b/sources/hyperdrive.js @@ -21,7 +21,7 @@ function hyperdriveSingle (handlers, opts) { var key - return { open, pull, transform } + return { open, pull } function open (next) { drive.ready(() => { @@ -46,6 +46,7 @@ function hyperdriveSingle (handlers, opts) { } function transform (msgs, next) { + console.log(msgs) for (const node of msgs) { if (_mountInfo) { node.name = p.join(_mountInfo.path, node.name) @@ -98,14 +99,15 @@ function hyperdriveMounts (handlers, opts) { const subdrive = hyperdrive(drive._corestore, key) subdrive.ready(() => { - addSource(key.toString('hex'), hyperdriveSingle, { - ...opts, - drive: subdrive, - _mountInfo: { - key: node.key, - path - } - }) + console.log('ADS', key.toString('hex', node.key), node.key, path) + // addSource(key.toString('hex'), hyperdriveSingle, { + // ...opts, + // drive: subdrive, + // _mountInfo: { + // key: node.key, + // path + // } + // }) done() }) }) diff --git a/sources/multifeed.js b/sources/multifeed.js index 31934fc..7aab9c1 100644 --- a/sources/multifeed.js +++ b/sources/multifeed.js @@ -1,13 +1,16 @@ const hypercoreSource = require('./hypercore') -const multi = require('./util/multisource') +const mergePull = require('./util/merge-pull') -module.exports = function multifeedSource (handlers, opts) { - const { pull, addSource } = multi(handlers) +module.exports = function multifeedSource (opts) { const multifeed = opts.feeds + const box = opts.box + let flow + const sources = [] return { pull, open } - function open (next) { + function open (_flow, next) { + flow = _flow multifeed.ready(() => { multifeed.feeds().forEach(feed => _onfeed(feed)) multifeed.on('feed', feed => _onfeed(feed)) @@ -15,9 +18,19 @@ module.exports = function multifeedSource (handlers, opts) { }) } + function pull (next) { + mergePull(sources, next) + } + function _onfeed (feed, cb) { feed.ready(() => { - addSource(feed.key.toString('hex'), hypercoreSource, { feed }) + const source = hypercoreSource({ + feed, + box, + prefix: feed.key.toString('hex') + }) + sources.push(source) + source.open(flow, cb) }) } } diff --git a/sources/util/merge-pull.js b/sources/util/merge-pull.js new file mode 100644 index 0000000..093089e --- /dev/null +++ b/sources/util/merge-pull.js @@ -0,0 +1,33 @@ +module.exports = function mergePull (sources, next) { + if (!sources.length) return next() + let results = [] + let pending = sources.length + sources.forEach(source => source.pull(onresult)) + function onresult (result) { + if (result) results.push(result) + if (--pending === 0) onfinish() + } + function onfinish () { + if (!results.length) return next() + let messages = [] + let finished = true + for (let result of results) { + if (result.messages) messages = messages.concat(result.messages) + if (!result.finished) finished = false + } + next({ + messages, + finished, + onindexed + }) + } + function onindexed (cb) { + let fns = results.map(r => r.onindexed).filter(f => f) + if (!fns.length) return cb() + let pending = fns.length + fns.forEach(fn => fn(done)) + function done () { + if (--pending === 0) cb() + } + } +} diff --git a/sources/util/multisource.js b/sources/util/multisource.js deleted file mode 100644 index e4031f2..0000000 --- a/sources/util/multisource.js +++ /dev/null @@ -1,43 +0,0 @@ -module.exports = function multiSource (handlers, opts) { - const sources = {} - const updateWanted = new Set() - - return { pull, addSource, sources } - - function addSource (name, createSource, opts, cb) { - if (sources[name]) return - const handlers = { - onupdate: onupdate.bind(onupdate, name) - } - const source = createSource(handlers, opts) - source.name = name - sources[name] = source - source.open(() => { - onupdate(name) - if (cb) cb() - }) - } - - function onupdate (name) { - updateWanted.add(name) - handlers.onupdate() - } - - function pull (state, next) { - state = state || {} - if (!updateWanted.size) return next(state) - - let results = [] - let pending = updateWanted.size - for (let name of updateWanted) { - updateWanted.delete(name) - sources[name].pull(state[name], done.bind(done, name)) - } - function done (name, nextState, msgs, moreWork) { - if (moreWork) updateWanted.add(name) - if (msgs) results = results.concat(msgs) - state[name] = nextState - if (--pending === 0) next(state, results, updateWanted.size) - } - } -} diff --git a/sources/util/state.js b/sources/util/state.js new file mode 100644 index 0000000..432cb4c --- /dev/null +++ b/sources/util/state.js @@ -0,0 +1,65 @@ +const Tinybox = require('tinybox') +const ram = require('random-access-memory') + +module.exports = class SimpleState { + constructor (opts = {}) { + this.box = opts.box || new Tinybox(ram()) + this.prefix = opts.prefix + } + + get _STATE () { + return this.prefix + '!state' + } + + get _VERSION () { + return this.prefix + '!version' + } + + prefix (prefix) { + return new SimpleState({ + box: this.box, + prefix: this.prefix + '/' + prefix + }) + } + + get (cb) { + getInt(this.box, this._STATE, cb) + } + + put (seq, cb) { + putInt(this.box, this._STATE, seq, cb) + } + + putVersion (version, cb) { + this.getVersion((err, lastVersion) => { + if (err) return cb(err) + if (version !== lastVersion) { + this.version = version + this.put(0, err => { + if (err) cb(err) + putInt(this.box, this._VERSION, version, cb) + }) + } + }) + } + + getVersion (cb) { + getInt(this.box, this._VERSION, cb) + } +} + +function getInt (db, key, cb) { + db.get(key, (err, node) => { + if (err || !node) return cb(err, 0) + const int = node.value.readInt32LE() || 0 + cb(null, int) + }) +} + +function putInt (db, key, int, cb) { + const buf = Buffer.allocUnsafe(4) + buf.writeInt32LE(int) + db.put(key, buf, cb || noop) +} + +function noop () {} diff --git a/test/basic.js b/test/basic.js index 56d6fca..82181cf 100644 --- a/test/basic.js +++ b/test/basic.js @@ -5,27 +5,22 @@ const { runAll } = require('./lib/util') tape('simple source', t => { const kappa = new Kappa() - const [source1, pushTo1] = makeSimpleSource() - const [source2, pushTo2] = makeSimpleSource() - kappa.source('s1', source1) - kappa.source('s2', source2) - kappa.use('view1', makeSimpleView()) - kappa.use('view2', makeSimpleView()) - - pushTo1(1) - pushTo1(2) - pushTo2(3) - pushTo2(4) + kappa.use('view1', makeSimpleSource(), makeSimpleView()) + kappa.use('view2', makeSimpleSource(), makeSimpleView()) + kappa.api.view1.source.push(1) + kappa.api.view1.source.push(2) + kappa.api.view2.source.push(3) + kappa.api.view2.source.push(4) runAll([ cb => kappa.api.view1.collect((err, res) => { t.error(err) - t.deepEqual(res, [1, 2, 3, 4]) + t.deepEqual(res, [1, 2]) cb() }), cb => kappa.api.view2.collect((err, res) => { t.error(err) - t.deepEqual(res, [1, 2, 3, 4]) + t.deepEqual(res, [3, 4]) cb() }), cb => t.end() @@ -48,27 +43,39 @@ function makeSimpleView () { return view } -function makeSimpleSource () { +function makeSimpleSource (opts = {}) { const buf = [] - const listeners = [] - - return [createSource, push] + const maxBatch = opts.maxBatch || 10 + let flow = null + let state = 0 - function createSource (handlers, opts) { - listeners.push(handlers.onupdate) - const maxBatch = opts.maxBatch || 2 - return { - pull (state, next) { - state = state || 0 - const end = Math.min(state + maxBatch, buf.length) - const slice = buf.slice(state, end) - next(end, slice, end < buf.length) + const source = { + open (_flow, next) { + flow = _flow + next() + }, + pull (next) { + const max = buf.length + const end = Math.min(state + maxBatch, max) + const messages = buf.slice(state, end) + next({ + messages, + finished: end === max, + onindexed (cb) { + state = end + cb() + } + }) + }, + get api () { + return { + push (kappa, value) { + buf.push(value) + if (flow) flow.update() + } } } } - function push (value) { - buf.push(value) - listeners.forEach(onupdate => onupdate()) - } + return source } diff --git a/test/corestore.js b/test/corestore.js index d352aed..219ab38 100644 --- a/test/corestore.js +++ b/test/corestore.js @@ -10,13 +10,10 @@ tape('corestore source', t => { const store = new Corestore(ram) store.ready(() => { const core1 = store.default({ valueEncoding: 'json' }) - - kappa.source('corestore', corestoreSource, { store }) - const core2 = store.get({ valueEncoding: 'json' }) let res = [] - kappa.use('view', { + kappa.use('view', corestoreSource({ store }), { map (msgs, next) { res = res.concat(msgs.map(msg => msg.value)) next() @@ -35,7 +32,7 @@ tape('corestore source', t => { setImmediate(() => { kappa.api.view.collect((err, res) => { t.error(err) - t.deepEqual(res, [1, 2, 3]) + t.deepEqual(res.sort(), [1, 2, 3]) t.end() }) }) diff --git a/test/hypercore.js b/test/hypercore.js index 9789f05..5d6a648 100644 --- a/test/hypercore.js +++ b/test/hypercore.js @@ -8,12 +8,9 @@ tape('hypercore source', t => { const kappa = new Kappa() const core1 = hypercore(ram, { valueEncoding: 'json' }) - const core2 = hypercore(ram, { valueEncoding: 'json' }) - kappa.source('core1', hypercoreSource, { feed: core1 }) - kappa.source('core2', hypercoreSource, { feed: core2 }) let res = [] - kappa.use('view', { + kappa.use('view', hypercoreSource({ feed: core1 }), { map (msgs, next) { res = res.concat(msgs.map(msg => msg.value)) next() @@ -26,7 +23,7 @@ tape('hypercore source', t => { }) core1.append(1) - core2.append(2) + core1.append(2) core1.append(3) setImmediate(() => { diff --git a/test/hyperdrive.js b/test/hyperdrive.js index 7b16ff4..0a276a7 100644 --- a/test/hyperdrive.js +++ b/test/hyperdrive.js @@ -9,7 +9,7 @@ const hyperdriveSource = require('../sources/hyperdrive') function corestore (storage) { return new Corestore(storage) } -tape('hyperdrive source', async t => { +tape.skip('hyperdrive source', async t => { const cstore = corestore(ram) var drive1, drive2, drive3, driveB, kappa, kappaB @@ -41,7 +41,7 @@ tape('hyperdrive source', async t => { cb => { kappa = new Kappa() kappa.use('files', makeFilesView()) - kappa.source('drive', hyperdriveSource, { drive: drive1 }) + kappa.source('drive', hyperdriveSource, { drive: drive1, mount: false }) kappa.pause() cb() }, @@ -100,7 +100,7 @@ tape('hyperdrive source', async t => { if (typeof msg === 'function') return testResult(kappa, '', msg) kappa.api.files.collect((err, res) => { t.error(err) - t.deepEqual(res, expected, msg) + t.deepEqual(res.sort(), expected.sort(), msg) cb() }) } diff --git a/test/multifeed.js b/test/multifeed.js index b87812c..4e743cb 100644 --- a/test/multifeed.js +++ b/test/multifeed.js @@ -1,22 +1,25 @@ -const test = require('tape') +const tape = require('tape') const ram = require('random-access-memory') -const kappa = require('..') +const { Kappa } = require('..') +const multifeed = require('multifeed') +const createMultifeedSource = require('../sources/multifeed') const { runAll } = require('./lib/util') -test('multifeed', async t => { - const core = kappa(ram, { valueEncoding: 'json' }) +tape('multifeed', async t => { + const feeds = multifeed(ram, { valueEncoding: 'json' }) + const kappa = new Kappa() - core.use('sum', createSumView()) + kappa.use('sum', createMultifeedSource({ feeds }), createSumView()) var feed1, feed2 await runAll([ - cb => core.writer('default', (err, feed) => { + cb => feeds.writer('default', (err, feed) => { t.error(err) feed1 = feed cb() }), - cb => core.writer('second', (err, feed) => { + cb => feeds.writer('second', (err, feed) => { t.error(err) feed2 = feed cb() @@ -25,7 +28,7 @@ test('multifeed', async t => { cb => feed1.append(1, cb), cb => feed2.append(3, cb), cb => { - core.api.sum.get(function (err, value) { + kappa.api.sum.get(function (err, value) { t.error(err) t.equals(5, value) cb() @@ -40,7 +43,7 @@ function createSumView () { let sum = 0 const sumview = { api: { - get: function (core, cb) { + get: function (kappa, cb) { this.ready(function () { cb(null, sum) }) diff --git a/test/stacked-view.js b/test/stacked-view.js index 5add3ae..da3977a 100644 --- a/test/stacked-view.js +++ b/test/stacked-view.js @@ -4,7 +4,8 @@ const hypercore = require('hypercore') const ram = require('random-access-memory') const hypercoreSource = require('../sources/hypercore') -tape('stacked views run in order', t => { +// Has to be updated. +tape.skip('stacked views run in order', t => { const kappa = new Kappa() const core = hypercore(ram, { valueEncoding: 'json' }) kappa.source('core', hypercoreSource, { feed: core }) @@ -64,7 +65,7 @@ tape('stacked views run in order', t => { }, 10) }) -tape('stacked view linked kv', t => { +tape.skip('stacked view linked kv', t => { const kappa = new Kappa() const core = hypercore(ram, { valueEncoding: 'json' }) kappa.source('core', hypercoreSource, { feed: core }) From 3cea9f7c979f5f38e08e2ece92ebb9c1e1547d13 Mon Sep 17 00:00:00 2001 From: "Franz Heinzmann (Frando)" Date: Thu, 5 Dec 2019 15:51:33 +0100 Subject: [PATCH 38/85] Finish API rework. Ready for review. --- README.md | 50 +++++----- kappa.js | 221 ++++++++++++++++++++---------------------- sources/corestore.js | 42 ++++---- sources/hypercore.js | 35 +++++-- sources/hyperdrive.js | 115 ---------------------- sources/multifeed.js | 57 ++++++----- sources/util/state.js | 45 +++++---- test/basic.js | 51 ++++++++-- test/corestore.js | 2 +- test/hypercore.js | 103 +++++++++++++++++++- test/hyperdrive.js | 125 ------------------------ test/multifeed.js | 2 +- 12 files changed, 389 insertions(+), 459 deletions(-) delete mode 100644 sources/hyperdrive.js delete mode 100644 test/hyperdrive.js diff --git a/README.md b/README.md index 892babf..73ad06d 100644 --- a/README.md +++ b/README.md @@ -18,49 +18,51 @@ Create a new kappa core. opts are: * `autoconnect: boolean`: connect all sources to all views (default: true) * `autostart: boolean`: start indexing immediately (default: true) - -#### `kappa.source(sourceName, createSource, opts)` - -Registers a new source. - -`createSource` is called with `(context, opts)` after the source is connected to a view. `context` is an object with: -* `view`: The view this source instance is connected to -* `onupdate`: A callback to signal to the kappa that this source has new data. This will trigger the `pull` function to be called (if processing is not paused). - -`createSource` has to return a source handler object with: - - #### `kappa.use(name, source, view)` -Register a view. viewName is a string, +Register a flow. -* `viewName` (string) the name of the view, has to be unique per kappa core -* `source` object with properties - * `open: function (flow, next)` (optional) +* `name` (string) the name of the flow, has to be unique per kappa core +* `source` object with properties: + * `open: function (flow, next)`: (optional) Handler to call on open * `pull: function (next)`: Pull new messages from the view. Should call `next` with either nothing or an object that looks like this: ```javascript { - messages: [], // array of messages, + messages: [], // array of messages finished: true, // if set to false, signal that more messages are pending onindexed: function (cb) {} // will be called when the view finished indexing } ``` - The source has to track its state, so that subsequent calls to `pull()` do not return the same messages. + The source has to track its state, so that subsequent calls to `pull()` do not return the same messages. Use the `onindexed` callback to update state. There are several source handlers included in kappa-core (TODO: document sources). See the tests and sources directories. - * `reset: function (cb) {}`: Reset internal state and restart indexing. + + * `reset: function (cb)`: Reset internal state (next pull should start at the beginning). + * `storeVersion: function (version, cb)`: Store the flow version somewhere + * `fetchVersion: function (cb)`: Fetch the version stored with storeVersion * `view` object with properties - * `open: function (flow, next)` (optional) * `map: function (msgs, next)` (required) - * `filter: function (msgs, next)` (optional), + * `open: function (flow, next)` (optional) + * `filter: function (msgs, next)` (optional) + * `version: int` + +Both `source` and `view` can have an `api` property with an object of function. The functions are exposed on `kappa.view[name]` / `kappa.source[name]`. Their `this` object refers to the flow, and their first parameter is the `kappa`. Other parameters are passed through. + +#### `kappa.reset(name, cb)` + +Reset a specific flow, to restart indexing. This is equal to reopening the kappa-core with a changed view version for this flow. + +#### `kappa.ready(names, cb)` -#### `kappa.clear(viewName)` +Call `cb` after all flows with a name in the `names` array have finished processing. If `names` is empty, all flows will be awaited. +#### `kappa.pause()` +Pause processing of all flows -## kappa classic +#### `kappa.resume()` -In `index.js` this new, dependencyless Kappa is wired to multifeed, creating an API compatible kappa-core. This is the default export at the moment. It passes `cabal-core` tests! And `multifeed-index` is not needed anymore even, just the two very simple functions in `sources/` (and the hypercoreSource can be used on its own also). +Resume processing of all flows --- diff --git a/kappa.js b/kappa.js index a267d19..e7e7c1c 100644 --- a/kappa.js +++ b/kappa.js @@ -18,45 +18,48 @@ module.exports = class Kappa extends EventEmitter { autostart: true, ...opts } - this.api = {} this.flows = {} this.open = thunky(this._open.bind(this)) + + // APIs + this.view = {} + this.source = {} } use (name, source, view, opts = {}) { - opts.name = name - const flow = new Flow(this, { source, view, opts, status: this.status }) + opts.status = opts.status || this.status + opts.context = opts.context || this + const flow = new Flow(name, source, view, opts) this.flows[name] = flow - this._assignApi(name, flow) - if (this.opts.autostart) flow.open() - this.emit('view', name) + this.view[name] = flow.view + this.source[name] = flow.source + flow.on('error', err => this.emit('error', err, flow)) + + if (this.opts.autostart) this.flows[name].open() + + this.emit('flow', name) + return flow } _open (cb) { - const flows = Object.values(this.flows) - let pending = flows.length - Object.values(flows).forEach(flow => { - flow.open(() => { - if (--pending === 0) cb() - }) - }) + let pending = Object.keys(this.flows.length) + if (!pending) return cb() + Object.values(this.flows).forEach(flow => flow.open(done)) + function done () { + if (--pending === 0) cb() + } } pause () { this.status = Status.Paused - const flows = Object.values(this.flows) - flows.forEach(flow => flow.pause()) + Object.values(this.flows).forEach(flow => flow.pause()) } resume () { - const flows = Object.values(this.flows) - if (this.status === Status.Paused) { - flows.forEach(flow => { - flow.resume() - }) - this.status = Status.Ready - } + if (this.status !== Status.Paused) return + Object.values(this.flows).forEach(flow => flow.resume()) + this.status = Status.Ready } reset (name, cb) { @@ -65,77 +68,81 @@ module.exports = class Kappa extends EventEmitter { flow.reset(cb) } - ready (viewNames, cb) { - if (typeof viewNames === 'function') return this.ready(null, viewNames) - if (typeof viewNames === 'string') viewNames = [viewNames] - if (!viewNames) viewNames = Object.keys(this.flows) + ready (names, cb) { + if (typeof names === 'function') return this.ready(null, names) + if (typeof names === 'string') names = [names] + if (!names) names = Object.keys(this.flows) // wait a tick process.nextTick(() => { - let pending = viewNames.length - viewNames.forEach(viewName => this._onViewIndexed(viewName, done)) + let pending = names.length + for (const name of names) { + const flow = this.flows[name] + if (!flow) return cb(new Error('Unknown flow: ' + name)) + flow.ready(done) + } function done () { if (--pending === 0) cb() } }) } - - _onViewIndexed (name, cb) { - const flow = this.flows[name] - if (!flow) return cb(new Error('Unknown view: ' + name)) - flow.ready(cb) - } - - _assignApi (name, flow) { - this.api[name] = { - name, - ready (cb) { - flow.ready(cb) - } - } - const context = this.opts.context || this - if (flow.view.api) { - for (let [key, value] of Object.entries(flow.view.api)) { - if (typeof value === 'function') value = value.bind(this.api[name], context) - this.api[name][key] = value - } - } - - if (flow.source.api) { - this.api[name].source = {} - for (let [key, value] of Object.entries(flow.source.api)) { - if (typeof value === 'function') value = value.bind(this.api[name], context) - this.api[name].source[key] = value - } - } - } } class Flow extends EventEmitter { - constructor (kappa, { source, view, opts = {}, status }) { + constructor (name, source, view, opts) { super() this.opts = opts - this.kappa = kappa - this.name = opts.name - this.view = view - this.source = source + this.name = name + + this._view = view + this._source = source + + this.context = opts.context + this.status = opts.status || Status.Ready + + // Assign view and source apis + this.view = {} + this.source = {} + if (view.api) { + for (let [key, value] of Object.entries(view.api)) { + this.view[key] = bindFn(value, this, this.context) + } + delete view.api + } + if (source.api) { + for (let [key, value] of Object.entries(source.api)) { + this.source[key] = bindFn(value, this, this.context) + } + delete source.api + } - this.status = status || Status.Ready this._opened = false this.open = thunky(this._open.bind(this)) } get version () { - return this.view.version + return this._view.version || 1 } _open (cb = noop) { if (this._opened) return cb() const self = this let pending = 1 - if (this.view.open) ++pending && this.view.open(this, done) - if (this.source.open) ++pending && this.source.open(this, done) + if (this._view.open) ++pending && this._view.open(this, done) + if (this._source.open) ++pending && this._source.open(this, done) + + if (this._source.fetchVersion) { + pending++ + this._source.fetchVersion((err, version) => { + if (err) return done() + if (!version) return this._source.storeVersion(this.version, done) + if (version !== this.version) { + this.reset(() => this._source.storeVersion(this.version, done)) + } else done() + }) + } + done() function done () { @@ -146,13 +153,10 @@ class Flow extends EventEmitter { } } - ready (cb = noop) { - const self = this - + ready (cb) { if (!this._opened) return this.open(() => this.ready(cb)) - - if (self.status === Status.Ready) process.nextTick(cb) - else self.once('ready', cb) + if (this.status === Status.Ready) process.nextTick(cb) + else this.once('ready', cb) } pause () { @@ -169,15 +173,16 @@ class Flow extends EventEmitter { reset (cb = noop) { const self = this this.pause() + let pending = 1 process.nextTick(() => { - if (this.view.clearIndex) this.view.clearIndex(reset) - else reset() + if (this._view.clearIndex) ++pending && this._view.clearIndex(done) + if (this._source.reset) ++pending && this._source.reset(done) + done() }) - function reset () { - self.source.reset(() => { - self.resume() - cb() - }) + function done () { + if (--pending !== 0) return + self.resume() + cb() } } @@ -187,14 +192,6 @@ class Flow extends EventEmitter { process.nextTick(this._run.bind(this)) } - _onbatch (msgs, cb) { - if (!msgs.length) return cb(null, msgs) - let prepare = [this.source.transform, this.opts.transform] - runThrough(msgs, prepare, msgs => { - applyView(this.view, msgs, () => cb(null, msgs)) - }) - } - _run () { const self = this if (!this._opened) throw new Error('Flow is not opened.') @@ -203,24 +200,35 @@ class Flow extends EventEmitter { this.status = Status.Running - this.source.pull(onbatch) + this._source.pull(onbatch) function onbatch (result) { - if (!result) return close() if (self.status === Status.Paused) return close() + if (!result || !result.messages.length) return close(null, result) + const { messages = [], finished, onindexed } = result - self._onbatch(messages, (err, messages) => { - if (err) return close(err) - close(null, { messages, finished, onindexed }) + + let steps = [ + self._source.transform, + self.opts.transform, + self._view.transform, + self._view.filter + ] + + runThrough(messages, steps, messages => { + if (!messages.length) return close(null, { messages, finished, onindexed }) + self._view.map(messages, () => { + close(null, { messages, finished, onindexed }) + }) }) } function close (err, result) { - if (err) self.kappa.emit('error', err, this) + if (err) self.emit('error', err) if (!result) return finish(true) const { messages, finished, onindexed } = result - if (messages.length && self.view.indexed) { - self.view.indexed(messages) + if (messages.length && self._view.indexed) { + self._view.indexed(messages) } if (onindexed) onindexed(() => finish(finished)) else finish(finished) @@ -238,12 +246,9 @@ class Flow extends EventEmitter { } } -function applyView (view, msgs, cb) { - const prepare = [view.filter, view.transform] - runThrough(msgs, prepare, (msgs) => { - if (!msgs.length) return cb() - view.map(msgs, cb) - }) +function bindFn (value, ...binds) { + if (typeof value === 'function') value = value.bind(...binds) + return value } function runThrough (state, fns, final) { @@ -258,16 +263,4 @@ function runThrough (state, fns, final) { } } -// function awaitAll (objs, fn, args, cb) { -// if (typeof args === 'function') return awaitAll(objs, fn, [], args) -// objs = objs.filter(obj => obj[fn]) -// let pending = objs.length -// if (!pending) return cb() -// args.push(done) -// objs.forEach(obj => obj[fn](...args)) -// function done () { -// if (--pending === 0) cb() -// } -// } - function noop () {} diff --git a/sources/corestore.js b/sources/corestore.js index e4025c7..20d4bfd 100644 --- a/sources/corestore.js +++ b/sources/corestore.js @@ -1,36 +1,42 @@ const hypercoreSource = require('./hypercore') const mergePull = require('./util/merge-pull') +const SimpleState = require('./util/state') module.exports = function corestoreSource (opts) { + const state = new SimpleState(opts) const store = opts.store const sources = [] - let flow = null return { - open (_flow, cb) { - flow = _flow + open (flow, cb) { store.ready(() => { - store.list().forEach(feed => _onfeed(feed)) - store.on('feed', feed => _onfeed(feed)) + store.list().forEach(feed => _onfeed(flow, feed)) + store.on('feed', feed => _onfeed(flow, feed)) cb() }) }, pull (next) { mergePull(sources, next) - } + }, + reset (cb) { + let pending = sources.length + sources.forEach(source => source.reset(done)) + function done () { + if (--pending === 0) cb() + } + }, + fetchVersion (cb) { state.fetchVersion(cb) }, + storeVersion (version, cb) { state.storeVersion(version, cb) } } - function _onfeed (feed, cb) { - feed.ready(() => { - const source = hypercoreSource({ - feed, - box: opts.box, - prefix: (opts.prefix || '') + flow.name + '!' + feed.key.toString('hex') - }) - sources.push(source) - source.open(flow, () => { - flow.update() - if (cb) cb() - }) + function _onfeed (flow, feed, cb) { + const source = hypercoreSource({ + feed, + state + }) + sources.push(source) + source.open(flow, () => { + flow.update() + if (cb) cb() }) } } diff --git a/sources/hypercore.js b/sources/hypercore.js index c191749..56c4dc5 100644 --- a/sources/hypercore.js +++ b/sources/hypercore.js @@ -1,33 +1,48 @@ -const State = require('./util/state') +const SimpleState = require('./util/state') module.exports = (...args) => new HypercoreSource(...args) class HypercoreSource { constructor (opts = {}) { - this.opts = opts this.feed = opts.feed this.maxBatch = opts.maxBatch || 50 + this.state = opts.state || new SimpleState(opts) } open (flow, cb) { this.flow = flow - this.state = new State({ - prefix: flow.name, - box: this.opts.box - }) this.feed.on('append', () => flow.update()) this.feed.on('download', () => flow.update()) - this.flow.update() - this.state.putVersion(flow.version, cb) + this.feed.ready(cb) + } + + fetchVersion (cb) { + this.state.fetchVersion(cb) + } + + storeVersion (version, cb) { + this.state.storeVersion(version, cb) + } + + reset (cb) { + this.state.set(0, cb) + } + + get name () { + return this.feed.key.toString('hex') } pull (cb) { - this.state.get((err, seq) => { + this.state.get(this.name, (err, seq) => { if (err) return cb(err) return this._pull(seq, cb) }) } + reset (cb) { + this.state.put(this.name, 0, cb) + } + _pull (at, next) { const self = this const feed = this.feed @@ -53,7 +68,7 @@ class HypercoreSource { messages: res, finished: to === len, onindexed (cb) { - self.state.put(to, cb) + self.state.put(self.name, to, cb) } }) }) diff --git a/sources/hyperdrive.js b/sources/hyperdrive.js deleted file mode 100644 index f9cd33e..0000000 --- a/sources/hyperdrive.js +++ /dev/null @@ -1,115 +0,0 @@ -const hyperdrive = require('hyperdrive') -const collect = require('stream-collector') -const p = require('path') - -const multi = require('./util/multisource') - -module.exports = function hyperdriveSource (handlers, opts) { - if (opts.mount === false) { - return hyperdriveSingle(handlers, opts) - } else { - return hyperdriveMounts(handlers, opts) - } -} - -function hyperdriveSingle (handlers, opts) { - const { - drive, - maxBatch = 100, - _mountInfo - } = opts - - var key - - return { open, pull } - - function open (next) { - drive.ready(() => { - drive.metadata.setMaxListeners(256) - key = drive.key.toString('hex') - drive.watch('/', handlers.onupdate) - next() - }) - } - - function pull (state, next) { - const at = state || 0 - const to = Math.min(at + maxBatch, drive.version) - if (to <= at) return next() - const diffStream = drive.checkout(to).createDiffStream(at) - collect(diffStream, (err, msgs) => { - if (err) return next(state) - transform(msgs, msgs => { - next(to, msgs, to < drive.version) - }) - }) - } - - function transform (msgs, next) { - console.log(msgs) - for (const node of msgs) { - if (_mountInfo) { - node.name = p.join(_mountInfo.path, node.name) - node.mountInfo = { - key, - path: _mountInfo.path - } - node.key = _mountInfo.key - } else { - node.key = key - } - } - next(msgs) - } -} - -function hyperdriveMounts (handlers, opts) { - const { pull, addSource } = multi(handlers, { open }) - const { drive } = opts - - return { pull, open, transform } - - function open (next) { - drive.ready(() => { - addSource(drive.key.toString('hex'), hyperdriveSingle, { drive }, next) - }) - } - - function transform (msgs, next) { - let pending = 1 - for (const node of msgs) { - if (node.type === 'mount') { - ++pending - mountToSource(node, done) - } - } - done() - function done () { - if (--pending === 0) next(msgs) - } - } - - function mountToSource (node, done) { - const path = node.name - drive.stat(path, (err, stat) => { - if (err || !stat || !stat.mount) return done() - const { key, hypercore } = stat.mount - // TODO: Support hypercore mounts? - if (hypercore) return done() - - const subdrive = hyperdrive(drive._corestore, key) - subdrive.ready(() => { - console.log('ADS', key.toString('hex', node.key), node.key, path) - // addSource(key.toString('hex'), hyperdriveSingle, { - // ...opts, - // drive: subdrive, - // _mountInfo: { - // key: node.key, - // path - // } - // }) - done() - }) - }) - } -} diff --git a/sources/multifeed.js b/sources/multifeed.js index 7aab9c1..9cae517 100644 --- a/sources/multifeed.js +++ b/sources/multifeed.js @@ -1,36 +1,43 @@ const hypercoreSource = require('./hypercore') const mergePull = require('./util/merge-pull') +const SimpleState = require('./util/state') module.exports = function multifeedSource (opts) { - const multifeed = opts.feeds - const box = opts.box - let flow + const state = new SimpleState(opts) + const feeds = opts.feeds const sources = [] - return { pull, open } - - function open (_flow, next) { - flow = _flow - multifeed.ready(() => { - multifeed.feeds().forEach(feed => _onfeed(feed)) - multifeed.on('feed', feed => _onfeed(feed)) - next() - }) - } - - function pull (next) { - mergePull(sources, next) + return { + open (flow, next) { + feeds.ready(() => { + feeds.feeds().forEach(feed => onfeed(flow, feed)) + feeds.on('feed', feed => onfeed(flow, feed)) + next() + }) + }, + pull (next) { + mergePull(sources, next) + }, + reset (cb) { + let pending = sources.length + sources.forEach(source => source.reset(done)) + function done () { + if (--pending === 0) cb() + } + }, + fetchVersion (cb) { state.fetchVersion(cb) }, + storeVersion (version, cb) { state.storeVersion(version, cb) } } - function _onfeed (feed, cb) { - feed.ready(() => { - const source = hypercoreSource({ - feed, - box, - prefix: feed.key.toString('hex') - }) - sources.push(source) - source.open(flow, cb) + function onfeed (flow, feed, cb) { + const source = hypercoreSource({ + feed, + state + }) + sources.push(source) + source.open(flow, () => { + flow.update() + if (cb) cb() }) } } diff --git a/sources/util/state.js b/sources/util/state.js index 432cb4c..e075973 100644 --- a/sources/util/state.js +++ b/sources/util/state.js @@ -8,11 +8,11 @@ module.exports = class SimpleState { } get _STATE () { - return this.prefix + '!state' + return this.prefix + '!state!' } get _VERSION () { - return this.prefix + '!version' + return this.prefix + '!version!' } prefix (prefix) { @@ -22,28 +22,23 @@ module.exports = class SimpleState { }) } - get (cb) { - getInt(this.box, this._STATE, cb) + get (name, cb) { + if (!cb) return this.get('', name) + const key = this._STATE + name + getInt(this.box, key, cb) } - put (seq, cb) { - putInt(this.box, this._STATE, seq, cb) + put (name, seq, cb) { + if (!cb) return this.put('', name, seq) + const key = this._STATE + name + putInt(this.box, key, seq, cb) } - putVersion (version, cb) { - this.getVersion((err, lastVersion) => { - if (err) return cb(err) - if (version !== lastVersion) { - this.version = version - this.put(0, err => { - if (err) cb(err) - putInt(this.box, this._VERSION, version, cb) - }) - } - }) + storeVersion (version, cb) { + putInt(this.box, this._VERSION, version, cb) } - getVersion (cb) { + fetchVersion (cb) { getInt(this.box, this._VERSION, cb) } } @@ -63,3 +58,17 @@ function putInt (db, key, int, cb) { } function noop () {} + +// module.exports = class StatefulSource { +// constructor (opts) { +// this.state = new SimpleState(opts) +// } + +// fetchVersion (cb) { +// this.state.getVersion(cb) +// } + +// storeVersion (version, cb) { +// this.state.setVersion(version, cb) +// } +// } diff --git a/test/basic.js b/test/basic.js index 82181cf..7e974c8 100644 --- a/test/basic.js +++ b/test/basic.js @@ -7,18 +7,18 @@ tape('simple source', t => { kappa.use('view1', makeSimpleSource(), makeSimpleView()) kappa.use('view2', makeSimpleSource(), makeSimpleView()) - kappa.api.view1.source.push(1) - kappa.api.view1.source.push(2) - kappa.api.view2.source.push(3) - kappa.api.view2.source.push(4) + kappa.source.view1.push(1) + kappa.source.view1.push(2) + kappa.source.view2.push(3) + kappa.source.view2.push(4) runAll([ - cb => kappa.api.view1.collect((err, res) => { + cb => kappa.view.view1.collect((err, res) => { t.error(err) t.deepEqual(res, [1, 2]) cb() }), - cb => kappa.api.view2.collect((err, res) => { + cb => kappa.view.view2.collect((err, res) => { t.error(err) t.deepEqual(res, [3, 4]) cb() @@ -27,16 +27,51 @@ tape('simple source', t => { ]) }) +tape('reset', t => { + const kappa = new Kappa() + const foo = kappa.use('foo', makeSimpleSource(), makeSimpleView()) + foo.source.push(1) + foo.source.push(2) + foo.source.push(3) + runAll([ + cb => foo.view.collect((err, res) => { + t.error(err) + t.deepEqual(res, [1, 2, 3]) + t.equal(kappa.view.foo.clearedCount(), 0) + cb() + }), + cb => { + kappa.reset('foo', cb) + }, + cb => foo.view.collect((err, res) => { + t.error(err) + t.deepEqual(res, [1, 2, 3]) + t.equal(kappa.view.foo.clearedCount(), 1) + cb() + }), + cb => t.end() + ]) +}) + function makeSimpleView () { let res = [] + let clears = 0 const view = { map (msgs, next) { res = res.concat(msgs) next() }, + clearIndex (cb) { + clears = clears + 1 + res = [] + cb() + }, api: { collect (kappa, cb) { this.ready(() => cb(null, res)) + }, + clearedCount (kappa) { + return clears } } } @@ -67,6 +102,10 @@ function makeSimpleSource (opts = {}) { } }) }, + reset (next) { + state = 0 + next() + }, get api () { return { push (kappa, value) { diff --git a/test/corestore.js b/test/corestore.js index 219ab38..75bce11 100644 --- a/test/corestore.js +++ b/test/corestore.js @@ -30,7 +30,7 @@ tape('corestore source', t => { core1.append(3) setImmediate(() => { - kappa.api.view.collect((err, res) => { + kappa.view.view.collect((err, res) => { t.error(err) t.deepEqual(res.sort(), [1, 2, 3]) t.end() diff --git a/test/hypercore.js b/test/hypercore.js index 5d6a648..e1f2807 100644 --- a/test/hypercore.js +++ b/test/hypercore.js @@ -1,16 +1,19 @@ const tape = require('tape') const { Kappa } = require('..') const hypercore = require('hypercore') +const Tinybox = require('tinybox') const ram = require('random-access-memory') const hypercoreSource = require('../sources/hypercore') +const { runAll } = require('./lib/util') tape('hypercore source', t => { const kappa = new Kappa() const core1 = hypercore(ram, { valueEncoding: 'json' }) + const state = new Tinybox(ram()) let res = [] - kappa.use('view', hypercoreSource({ feed: core1 }), { + kappa.use('view', hypercoreSource({ feed: core1, box: state }), { map (msgs, next) { res = res.concat(msgs.map(msg => msg.value)) next() @@ -27,10 +30,106 @@ tape('hypercore source', t => { core1.append(3) setImmediate(() => { - kappa.api.view.collect((err, res) => { + kappa.view.view.collect((err, res) => { t.error(err) t.deepEqual(res, [1, 2, 3]) t.end() }) }) }) + +tape('versions', t => { + const feed = hypercore(ram, { valueEncoding: 'json' }) + const sourceState = new Tinybox(ram()) + const viewState = new Tinybox(ram()) + + function createKappa (feed, version) { + const kappa = new Kappa() + const source = hypercoreSource({ feed, box: sourceState }) + const view = makeSimpleView(viewState, version) + const opts = { + transform (msgs, next) { + next(msgs.map(msg => msg.value)) + } + } + kappa.use('foo', source, view, opts) + return kappa + } + + feed.append('a') + feed.append('b') + + let kappa = createKappa(feed, 1) + let kappa2 + + runAll([ + cb => setImmediate(cb), + cb => { + kappa.view.foo.collect((err, res) => { + t.error(err) + t.deepEqual(res, ['av1', 'bv1'], 'first round ok') + cb() + }) + }, + cb => { + kappa.pause() + kappa = createKappa(feed, 1) + cb() + }, + cb => { + kappa.view.foo.collect((err, res) => { + t.error(err) + t.deepEqual(res, ['av1', 'bv1'], 'second round ok') + cb() + }) + }, + cb => { + kappa.pause() + kappa = createKappa(feed, 2) + cb() + }, + cb => { + kappa.view.foo.collect((err, res) => { + t.error(err) + t.deepEqual(res, ['av2', 'bv2'], 'second round ok') + cb() + }) + }, + cb => t.end() + ]) +}) + +function makeSimpleView (db, version) { + let clears = 0 + const view = { + map (msgs, next) { + msgs = msgs.map(str => { + return str + 'v' + version + }) + db.get('msgs', (err, node) => { + if (err) return next() + let value = node && node.value ? JSON.parse(node.value) : [] + value = value.concat(msgs) + db.put('msgs', JSON.stringify(value), next) + }) + }, + version, + clearIndex (cb) { + clears = clears + 1 + db.put('msgs', JSON.stringify([], cb)) + }, + api: { + collect (kappa, cb) { + this.ready(() => { + db.get('msgs', (err, node) => { + cb(err, node ? JSON.parse(node.value) : []) + }) + }) + }, + clearedCount (kappa) { + return clears + } + } + } + return view +} diff --git a/test/hyperdrive.js b/test/hyperdrive.js deleted file mode 100644 index 0a276a7..0000000 --- a/test/hyperdrive.js +++ /dev/null @@ -1,125 +0,0 @@ -const tape = require('tape') -const ram = require('random-access-memory') -const hyperdrive = require('hyperdrive') -const Corestore = require('corestore') -const { runAll, replicate } = require('./lib/util') - -const { Kappa } = require('..') -const hyperdriveSource = require('../sources/hyperdrive') - -function corestore (storage) { return new Corestore(storage) } - -tape.skip('hyperdrive source', async t => { - const cstore = corestore(ram) - - var drive1, drive2, drive3, driveB, kappa, kappaB - - const expected = [ - 'put hello1', - 'mount in2', - 'mount in2/in3', - 'put in2/hello2', - 'put in2/in3/hello3' - ] - - await runAll([ - // init all the drives - cb => { - drive1 = hyperdrive(cstore) - drive1.ready(cb) - }, - cb => { - drive2 = hyperdrive(cstore) - drive2.ready(cb) - }, - cb => { - drive3 = hyperdrive(cstore) - drive3.ready(cb) - }, - - // open first kappa - cb => { - kappa = new Kappa() - kappa.use('files', makeFilesView()) - kappa.source('drive', hyperdriveSource, { drive: drive1, mount: false }) - kappa.pause() - cb() - }, - - // mounts and writeFiles - cb => drive1.writeFile('hello1', Buffer.from('world1'), cb), - cb => drive1.writeFile('hello1_del', Buffer.from('world1'), cb), - cb => drive2.writeFile('hello2', Buffer.from('world2'), cb), - cb => drive3.writeFile('hello3', Buffer.from('world3'), cb), - cb => drive1.mount('in2', drive2.key, cb), - cb => drive2.mount('in3', drive3.key, cb), - cb => drive1.unlink('hello1_del', cb), - - // resume kappa so that the indexes are built - cb => { - kappa.resume() - cb() - }, - - cb => testResult(kappa, 'original kappa matches', cb) - ]) - - await runAll([ - // pause the kappa so that intermediate diffs are skipped - // in a real-world scenario the views would have be aware - // of the linear, but not synonym history of hyperdrives - // (when using the diff iterator) - cb => { - kappa.resume() - cb() - }, - - // open second (remote) drive - cb => { - driveB = hyperdrive(ram, drive1.key) - driveB.ready(cb) - }, - - // init second (remote) kappa - cb => { - kappaB = new Kappa() - kappaB.use('files', makeFilesView()) - kappaB.source('drive', hyperdriveSource, { drive: driveB }) - cb() - }, - - // replicate the local and remote drives - cb => replicate(drive1, driveB, cb), - - cb => testResult(kappaB, 'replicated kappa matches', cb) - ]) - - t.end() - - function testResult (kappa, msg, cb) { - if (typeof msg === 'function') return testResult(kappa, '', msg) - kappa.api.files.collect((err, res) => { - t.error(err) - t.deepEqual(res.sort(), expected.sort(), msg) - cb() - }) - } -}) - -function makeFilesView () { - let list = [] - return { - map (msgs, next) { - list.push(...msgs) - next() - }, - api: { - collect (kappa, cb) { - this.ready(async () => { - const res = list.map(node => `${node.type} ${node.name}`) - cb(null, res) - }) - } - } - } -} diff --git a/test/multifeed.js b/test/multifeed.js index 4e743cb..bf20835 100644 --- a/test/multifeed.js +++ b/test/multifeed.js @@ -28,7 +28,7 @@ tape('multifeed', async t => { cb => feed1.append(1, cb), cb => feed2.append(3, cb), cb => { - kappa.api.sum.get(function (err, value) { + kappa.view.sum.get(function (err, value) { t.error(err) t.equals(5, value) cb() From e239f0ec81fbd65623247a526abd824a592ead70 Mon Sep 17 00:00:00 2001 From: "Franz Heinzmann (Frando)" Date: Thu, 5 Dec 2019 16:15:54 +0100 Subject: [PATCH 39/85] version --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 2657134..d424b33 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { "name": "kappa-core", "description": "Minimal peer-to-peer database, based on kappa architecture.", - "author": "Stephen Whitmore ", - "version": "5.0.0-alpha1", + "author": "Stephen Whitmore , Franz Heinzmann ", + "version": "7.0.0-alpha1", "repository": { "url": "git://github.com/noffle/kappa-core.git" }, From f6b7705759a35f08a203078535e3f12b6532b1f3 Mon Sep 17 00:00:00 2001 From: "Franz Heinzmann (Frando)" Date: Fri, 6 Dec 2019 18:47:00 +0100 Subject: [PATCH 40/85] Use level-mem for state helper --- kappa.js | 1 + package.json | 3 ++- sources/corestore.js | 6 ++++-- sources/util/state.js | 43 +++++++++++++++++-------------------------- test/corestore.js | 4 +++- test/hypercore.js | 25 ++++++++++++------------- test/multifeed.js | 7 +++++-- 7 files changed, 44 insertions(+), 45 deletions(-) diff --git a/kappa.js b/kappa.js index e7e7c1c..b166836 100644 --- a/kappa.js +++ b/kappa.js @@ -104,6 +104,7 @@ class Flow extends EventEmitter { // Assign view and source apis this.view = {} this.source = {} + this.view.ready = cb => this.ready(cb) if (view.api) { for (let [key, value] of Object.entries(view.api)) { this.view[key] = bindFn(value, this, this.context) diff --git a/package.json b/package.json index d424b33..d2340fd 100644 --- a/package.json +++ b/package.json @@ -20,12 +20,13 @@ "thunky": "^1.1.0" }, "devDependencies": { - "multifeed": "^5", "bitfield-db": "^1.2.2", "corestore": "^4.0.0", "hypercore": "^8", "hyperdrive": "github:mafintosh/hyperdrive#hypercore-8", "hyperdrive-schemas": "^0.12.0", + "level-mem": "^5.0.1", + "multifeed": "^5", "nanoiterator": "^1.2.0", "random-access-memory": "^3.1.1", "standard": "~12.0.1", diff --git a/sources/corestore.js b/sources/corestore.js index 20d4bfd..f023844 100644 --- a/sources/corestore.js +++ b/sources/corestore.js @@ -3,7 +3,7 @@ const mergePull = require('./util/merge-pull') const SimpleState = require('./util/state') module.exports = function corestoreSource (opts) { - const state = new SimpleState(opts) + const state = opts.state || new SimpleState(opts) const store = opts.store const sources = [] return { @@ -15,7 +15,9 @@ module.exports = function corestoreSource (opts) { }) }, pull (next) { - mergePull(sources, next) + mergePull(sources, results => { + next(results) + }) }, reset (cb) { let pending = sources.length diff --git a/sources/util/state.js b/sources/util/state.js index e075973..f7c8920 100644 --- a/sources/util/state.js +++ b/sources/util/state.js @@ -1,60 +1,51 @@ -const Tinybox = require('tinybox') -const ram = require('random-access-memory') - module.exports = class SimpleState { constructor (opts = {}) { - this.box = opts.box || new Tinybox(ram()) - this.prefix = opts.prefix - } - - get _STATE () { - return this.prefix + '!state!' - } - - get _VERSION () { - return this.prefix + '!version!' + this.db = opts.db + this._prefix = opts._prefix || '' + this._STATE = this._prefix + '!state!' + this._VERSION = this._prefix + '!version!' } prefix (prefix) { return new SimpleState({ - box: this.box, - prefix: this.prefix + '/' + prefix + db: this.db, + prefix: this._prefix + '/' + prefix }) } get (name, cb) { if (!cb) return this.get('', name) const key = this._STATE + name - getInt(this.box, key, cb) + getInt(this.db, key, cb) } put (name, seq, cb) { if (!cb) return this.put('', name, seq) const key = this._STATE + name - putInt(this.box, key, seq, cb) + putInt(this.db, key, seq, cb) } storeVersion (version, cb) { - putInt(this.box, this._VERSION, version, cb) + putInt(this.db, this._VERSION, version, cb) } fetchVersion (cb) { - getInt(this.box, this._VERSION, cb) + getInt(this.db, this._VERSION, cb) } } function getInt (db, key, cb) { - db.get(key, (err, node) => { - if (err || !node) return cb(err, 0) - const int = node.value.readInt32LE() || 0 - cb(null, int) + db.get(key, (err, value) => { + if (err && err.type !== 'NotFoundError') return cb(err) + if (!value) return cb(null, 0) + value = Number(value) + cb(null, value) }) } function putInt (db, key, int, cb) { - const buf = Buffer.allocUnsafe(4) - buf.writeInt32LE(int) - db.put(key, buf, cb || noop) + const value = String(int) + db.put(key, value, cb || noop) } function noop () {} diff --git a/test/corestore.js b/test/corestore.js index 75bce11..034b6c4 100644 --- a/test/corestore.js +++ b/test/corestore.js @@ -2,6 +2,7 @@ const tape = require('tape') const { Kappa } = require('..') const Corestore = require('corestore') const ram = require('random-access-memory') +const mem = require('level-mem') const corestoreSource = require('../sources/corestore') tape('corestore source', t => { @@ -11,9 +12,10 @@ tape('corestore source', t => { store.ready(() => { const core1 = store.default({ valueEncoding: 'json' }) const core2 = store.get({ valueEncoding: 'json' }) + const db = mem() let res = [] - kappa.use('view', corestoreSource({ store }), { + kappa.use('view', corestoreSource({ store, db }), { map (msgs, next) { res = res.concat(msgs.map(msg => msg.value)) next() diff --git a/test/hypercore.js b/test/hypercore.js index e1f2807..f469e7d 100644 --- a/test/hypercore.js +++ b/test/hypercore.js @@ -1,7 +1,7 @@ const tape = require('tape') const { Kappa } = require('..') const hypercore = require('hypercore') -const Tinybox = require('tinybox') +const mem = require('level-mem') const ram = require('random-access-memory') const hypercoreSource = require('../sources/hypercore') const { runAll } = require('./lib/util') @@ -10,10 +10,10 @@ tape('hypercore source', t => { const kappa = new Kappa() const core1 = hypercore(ram, { valueEncoding: 'json' }) - const state = new Tinybox(ram()) + const statedb = mem() let res = [] - kappa.use('view', hypercoreSource({ feed: core1, box: state }), { + kappa.use('view', hypercoreSource({ feed: core1, db: statedb }), { map (msgs, next) { res = res.concat(msgs.map(msg => msg.value)) next() @@ -40,12 +40,12 @@ tape('hypercore source', t => { tape('versions', t => { const feed = hypercore(ram, { valueEncoding: 'json' }) - const sourceState = new Tinybox(ram()) - const viewState = new Tinybox(ram()) + const sourceState = mem() + const viewState = mem() function createKappa (feed, version) { const kappa = new Kappa() - const source = hypercoreSource({ feed, box: sourceState }) + const source = hypercoreSource({ feed, db: sourceState }) const view = makeSimpleView(viewState, version) const opts = { transform (msgs, next) { @@ -60,7 +60,6 @@ tape('versions', t => { feed.append('b') let kappa = createKappa(feed, 1) - let kappa2 runAll([ cb => setImmediate(cb), @@ -106,9 +105,9 @@ function makeSimpleView (db, version) { msgs = msgs.map(str => { return str + 'v' + version }) - db.get('msgs', (err, node) => { - if (err) return next() - let value = node && node.value ? JSON.parse(node.value) : [] + db.get('msgs', (err, value) => { + if (err && !err.notFound) return next() + value = value ? JSON.parse(value) : [] value = value.concat(msgs) db.put('msgs', JSON.stringify(value), next) }) @@ -116,13 +115,13 @@ function makeSimpleView (db, version) { version, clearIndex (cb) { clears = clears + 1 - db.put('msgs', JSON.stringify([], cb)) + db.put('msgs', JSON.stringify([]), cb) }, api: { collect (kappa, cb) { this.ready(() => { - db.get('msgs', (err, node) => { - cb(err, node ? JSON.parse(node.value) : []) + db.get('msgs', (err, value) => { + cb(err, value ? JSON.parse(value) : []) }) }) }, diff --git a/test/multifeed.js b/test/multifeed.js index bf20835..8ec0e6e 100644 --- a/test/multifeed.js +++ b/test/multifeed.js @@ -1,15 +1,18 @@ const tape = require('tape') const ram = require('random-access-memory') -const { Kappa } = require('..') const multifeed = require('multifeed') +const mem = require('level-mem') + +const { Kappa } = require('..') const createMultifeedSource = require('../sources/multifeed') const { runAll } = require('./lib/util') tape('multifeed', async t => { const feeds = multifeed(ram, { valueEncoding: 'json' }) const kappa = new Kappa() + const db = mem() - kappa.use('sum', createMultifeedSource({ feeds }), createSumView()) + kappa.use('sum', createMultifeedSource({ feeds, db }), createSumView()) var feed1, feed2 From 7fa47584a18c346033846470d9c11b550f98e20a Mon Sep 17 00:00:00 2001 From: "Franz Heinzmann (Frando)" Date: Fri, 6 Dec 2019 18:49:12 +0100 Subject: [PATCH 41/85] Cleanup --- test/basic.js | 10 +++++----- test/corestore.js | 28 ++++++++++++++++------------ 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/test/basic.js b/test/basic.js index 7e974c8..1badccd 100644 --- a/test/basic.js +++ b/test/basic.js @@ -5,8 +5,8 @@ const { runAll } = require('./lib/util') tape('simple source', t => { const kappa = new Kappa() - kappa.use('view1', makeSimpleSource(), makeSimpleView()) - kappa.use('view2', makeSimpleSource(), makeSimpleView()) + kappa.use('view1', createSimpleSource(), createSimpleView()) + kappa.use('view2', createSimpleSource(), createSimpleView()) kappa.source.view1.push(1) kappa.source.view1.push(2) kappa.source.view2.push(3) @@ -29,7 +29,7 @@ tape('simple source', t => { tape('reset', t => { const kappa = new Kappa() - const foo = kappa.use('foo', makeSimpleSource(), makeSimpleView()) + const foo = kappa.use('foo', createSimpleSource(), createSimpleView()) foo.source.push(1) foo.source.push(2) foo.source.push(3) @@ -53,7 +53,7 @@ tape('reset', t => { ]) }) -function makeSimpleView () { +function createSimpleView () { let res = [] let clears = 0 const view = { @@ -78,7 +78,7 @@ function makeSimpleView () { return view } -function makeSimpleSource (opts = {}) { +function createSimpleSource (opts = {}) { const buf = [] const maxBatch = opts.maxBatch || 10 let flow = null diff --git a/test/corestore.js b/test/corestore.js index 034b6c4..0ed3af6 100644 --- a/test/corestore.js +++ b/test/corestore.js @@ -14,18 +14,7 @@ tape('corestore source', t => { const core2 = store.get({ valueEncoding: 'json' }) const db = mem() - let res = [] - kappa.use('view', corestoreSource({ store, db }), { - map (msgs, next) { - res = res.concat(msgs.map(msg => msg.value)) - next() - }, - api: { - collect (kappa, cb) { - this.ready(() => cb(null, res)) - } - } - }) + kappa.use('view', corestoreSource({ store, db }), createSimpleView()) core1.append(1) core2.append(2) @@ -40,3 +29,18 @@ tape('corestore source', t => { }) }) }) + +function createSimpleView () { + let res = [] + return { + map (msgs, next) { + res = res.concat(msgs.map(msg => msg.value)) + next() + }, + api: { + collect (kappa, cb) { + this.ready(() => cb(null, res)) + } + } + } +} From 093d9519c9cfdafddf38e77795d20777fb11e4a5 Mon Sep 17 00:00:00 2001 From: "Franz Heinzmann (Frando)" Date: Sat, 7 Dec 2019 12:18:47 +0100 Subject: [PATCH 42/85] Improve source utils --- sources/corestore.js | 18 ++++------- sources/multifeed.js | 14 +++------ sources/util/{merge-pull.js => merge.js} | 10 +++++- sources/util/state.js | 39 ++++++++++++++++-------- 4 files changed, 46 insertions(+), 35 deletions(-) rename sources/util/{merge-pull.js => merge.js} (78%) diff --git a/sources/corestore.js b/sources/corestore.js index f023844..c89db36 100644 --- a/sources/corestore.js +++ b/sources/corestore.js @@ -1,5 +1,5 @@ const hypercoreSource = require('./hypercore') -const mergePull = require('./util/merge-pull') +const { mergePull, mergeReset } = require('./util/merge') const SimpleState = require('./util/state') module.exports = function corestoreSource (opts) { @@ -15,19 +15,13 @@ module.exports = function corestoreSource (opts) { }) }, pull (next) { - mergePull(sources, results => { - next(results) - }) + mergePull(sources, next) }, - reset (cb) { - let pending = sources.length - sources.forEach(source => source.reset(done)) - function done () { - if (--pending === 0) cb() - } + reset (next) { + mergeReset(sources, next) }, - fetchVersion (cb) { state.fetchVersion(cb) }, - storeVersion (version, cb) { state.storeVersion(version, cb) } + fetchVersion: state.fetchVersion, + storeVersion: state.storeVersion } function _onfeed (flow, feed, cb) { diff --git a/sources/multifeed.js b/sources/multifeed.js index 9cae517..edac02a 100644 --- a/sources/multifeed.js +++ b/sources/multifeed.js @@ -1,5 +1,5 @@ const hypercoreSource = require('./hypercore') -const mergePull = require('./util/merge-pull') +const { mergePull, mergeReset } = require('./util/merge') const SimpleState = require('./util/state') module.exports = function multifeedSource (opts) { @@ -18,15 +18,11 @@ module.exports = function multifeedSource (opts) { pull (next) { mergePull(sources, next) }, - reset (cb) { - let pending = sources.length - sources.forEach(source => source.reset(done)) - function done () { - if (--pending === 0) cb() - } + reset (next) { + mergeReset(sources, next) }, - fetchVersion (cb) { state.fetchVersion(cb) }, - storeVersion (version, cb) { state.storeVersion(version, cb) } + fetchVersion: state.fetchVersion, + storeVersion: state.storeVersion } function onfeed (flow, feed, cb) { diff --git a/sources/util/merge-pull.js b/sources/util/merge.js similarity index 78% rename from sources/util/merge-pull.js rename to sources/util/merge.js index 093089e..db80cc7 100644 --- a/sources/util/merge-pull.js +++ b/sources/util/merge.js @@ -1,4 +1,4 @@ -module.exports = function mergePull (sources, next) { +exports.mergePull = function (sources, next) { if (!sources.length) return next() let results = [] let pending = sources.length @@ -31,3 +31,11 @@ module.exports = function mergePull (sources, next) { } } } + +exports.mergeReset = function (sources, cb) { + let pending = sources.length + sources.forEach(source => source.reset(done)) + function done () { + if (--pending === 0) cb() + } +} diff --git a/sources/util/state.js b/sources/util/state.js index f7c8920..b828ce1 100644 --- a/sources/util/state.js +++ b/sources/util/state.js @@ -1,9 +1,14 @@ module.exports = class SimpleState { constructor (opts = {}) { - this.db = opts.db - this._prefix = opts._prefix || '' + this.db = opts.db || new FakeDB() + this._prefix = opts.prefix || '' this._STATE = this._prefix + '!state!' this._VERSION = this._prefix + '!version!' + // Bind public methods so that they can be passed on directly. + this.get = this.get.bind(this) + this.put = this.put.bind(this) + this.storeVersion = this.storeVersion.bind(this) + this.fetchVersion = this.fetchVersion.bind(this) } prefix (prefix) { @@ -50,16 +55,24 @@ function putInt (db, key, int, cb) { function noop () {} -// module.exports = class StatefulSource { -// constructor (opts) { -// this.state = new SimpleState(opts) -// } +class FakeDB { + constructor () { + this.state = {} + } -// fetchVersion (cb) { -// this.state.getVersion(cb) -// } + put (key, value, cb) { + this.state[key] = value + cb() + } -// storeVersion (version, cb) { -// this.state.setVersion(version, cb) -// } -// } + get (key, cb) { + if (typeof this.state[key] === 'undefined') { + const err = new Error('Key not found') + err.type = 'NotFoundError' + err.notFound = true + cb(err) + } else { + cb(null, this.state[key]) + } + } +} From f3c8d7c59990706f9fb457e66db3789e7397dd98 Mon Sep 17 00:00:00 2001 From: "Franz Heinzmann (Frando)" Date: Sat, 7 Dec 2019 16:28:23 +0100 Subject: [PATCH 43/85] Add feed api on multifeed and corestore sources --- sources/corestore.js | 7 ++++++- sources/multifeed.js | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/sources/corestore.js b/sources/corestore.js index c89db36..30c6eab 100644 --- a/sources/corestore.js +++ b/sources/corestore.js @@ -21,7 +21,12 @@ module.exports = function corestoreSource (opts) { mergeReset(sources, next) }, fetchVersion: state.fetchVersion, - storeVersion: state.storeVersion + storeVersion: state.storeVersion, + api: { + feed (key) { + return store.get({ key }) + } + } } function _onfeed (flow, feed, cb) { diff --git a/sources/multifeed.js b/sources/multifeed.js index edac02a..8c0befb 100644 --- a/sources/multifeed.js +++ b/sources/multifeed.js @@ -22,7 +22,12 @@ module.exports = function multifeedSource (opts) { mergeReset(sources, next) }, fetchVersion: state.fetchVersion, - storeVersion: state.storeVersion + storeVersion: state.storeVersion, + api: { + feed (key) { + return feeds.feed(key) + } + } } function onfeed (flow, feed, cb) { From 70d871707ee89934fbf5162d43a66380614bfdb7 Mon Sep 17 00:00:00 2001 From: "Franz Heinzmann (Frando)" Date: Sat, 7 Dec 2019 16:31:18 +0100 Subject: [PATCH 44/85] fix --- sources/corestore.js | 2 +- sources/multifeed.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sources/corestore.js b/sources/corestore.js index 30c6eab..4dbd8ea 100644 --- a/sources/corestore.js +++ b/sources/corestore.js @@ -23,7 +23,7 @@ module.exports = function corestoreSource (opts) { fetchVersion: state.fetchVersion, storeVersion: state.storeVersion, api: { - feed (key) { + feed (kappa, key) { return store.get({ key }) } } diff --git a/sources/multifeed.js b/sources/multifeed.js index 8c0befb..aa71dee 100644 --- a/sources/multifeed.js +++ b/sources/multifeed.js @@ -24,7 +24,7 @@ module.exports = function multifeedSource (opts) { fetchVersion: state.fetchVersion, storeVersion: state.storeVersion, api: { - feed (key) { + feed (kappa, key) { return feeds.feed(key) } } From 373023f54347d18f4d359a05392aa09aa908154f Mon Sep 17 00:00:00 2001 From: "Franz Heinzmann (Frando)" Date: Sat, 7 Dec 2019 16:47:52 +0100 Subject: [PATCH 45/85] fix --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 73ad06d..d22a714 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,6 @@ Make the base Kappa core dependencyless. See `./kappa.js`. `Kappa` just deals with *sources* and *views*. Sources pull values based on their last state, views process values in a map function. -![kappa flow](kappa-graph.svg) - ## API `const { Kappa } = require('kappa-core')` From 78f7e11dc1d55669efd282e66cbaf28fe6a11a68 Mon Sep 17 00:00:00 2001 From: "Franz Heinzmann (Frando)" Date: Sat, 7 Dec 2019 18:30:41 +0100 Subject: [PATCH 46/85] Streamline and remove unneeded opts --- kappa.js | 22 +++++----------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/kappa.js b/kappa.js index b166836..773cd1d 100644 --- a/kappa.js +++ b/kappa.js @@ -14,18 +14,15 @@ module.exports = class Kappa extends EventEmitter { */ constructor (opts = {}) { super() - this.opts = { - autostart: true, - ...opts - } this.flows = {} - this.open = thunky(this._open.bind(this)) - // APIs this.view = {} this.source = {} } + // This is here for backwards compatibility. + get api () { return this.view } + use (name, source, view, opts = {}) { opts.status = opts.status || this.status opts.context = opts.context || this @@ -35,20 +32,11 @@ module.exports = class Kappa extends EventEmitter { this.source[name] = flow.source flow.on('error', err => this.emit('error', err, flow)) - if (this.opts.autostart) this.flows[name].open() - this.emit('flow', name) - return flow - } + if (this.status !== Status.Paused) flow.open() - _open (cb) { - let pending = Object.keys(this.flows.length) - if (!pending) return cb() - Object.values(this.flows).forEach(flow => flow.open(done)) - function done () { - if (--pending === 0) cb() - } + return flow } pause () { From 3c6b8eabf0826643bcc0a3569d6c7a48b29baf59 Mon Sep 17 00:00:00 2001 From: "Franz Heinzmann (Frando)" Date: Sat, 7 Dec 2019 22:36:28 +0100 Subject: [PATCH 47/85] docs --- README.md | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index d22a714..9d17c82 100644 --- a/README.md +++ b/README.md @@ -2,19 +2,17 @@ ## **kappa-core WIP rewrite** ## -Make the base Kappa core dependencyless. +kappa-core is a database abstraction for append-only logs and add-only sets. A kappa-core is a container for pairs of sources and views, called *flows*. In each flow, data flows from a source into a view. Sources have a pull function that fetches new messages since the last pull. Views have a map function which is called for each batch of messages from the source. -See `./kappa.js`. `Kappa` just deals with *sources* and *views*. Sources pull values based on their last state, views process values in a map function. +kappa-core itself is dependencyless, but this module also contains stateful source handlers for [hypercores](https://github.com/mafintosh/hypercore), [multifeed](https://github.com/kappa-core/multifeed), and [corestore](https://github.com/andrewosh/corestore). ## API `const { Kappa } = require('kappa-core')` -#### `const kappa = new Kappa(opts)` +#### `const kappa = new Kappa()` -Create a new kappa core. opts are: -* `autoconnect: boolean`: connect all sources to all views (default: true) -* `autostart: boolean`: start indexing immediately (default: true) +Create a new kappa core. #### `kappa.use(name, source, view)` @@ -35,7 +33,9 @@ Register a flow. There are several source handlers included in kappa-core (TODO: document sources). See the tests and sources directories. - * `reset: function (cb)`: Reset internal state (next pull should start at the beginning). + A simple state handler that perists state in a leveldb (or in memory) is included and used by the bundled source handler. It's available for use by custom sources on `Kappa.SimpleState`. If the bundled source handlers get a `db` option passed with levelup instance, the source state will be persisted. + + * `reset: function (cb)`: Reset internal state (next pull should start at the beginning). This handler is called by the kappa-core if the view's `version` property increases and thus wants to restart indexing. * `storeVersion: function (version, cb)`: Store the flow version somewhere * `fetchVersion: function (cb)`: Fetch the version stored with storeVersion * `view` object with properties @@ -62,6 +62,26 @@ Pause processing of all flows Resume processing of all flows +## Sources + +* hypercore + + `const createHypercoreSource = require('kappa-core/sources/hypercore')` + `createHypercoreSource({ feed, db })` + where `feed` is a hypercore instance and `db` is a levelup instance (for persisting state) + +* multifeed + + `const createMultifeedSource = require('kappa-core/sources/multifeed')` + `createHypercoreSource({ feeds, db })` + where `feeds` is a multifeed instance and `db` is a levelup instance (for persisting state) + +* corestore + + `const createCorestoreSource = require('kappa-core/sources/corestore')` + `createHypercoreSource({ store, db })` + where `store` is a corestore instance and `db` is a levelup instance (for persisting state) + --- > kappa-core is a minimal peer-to-peer database, based on append-only logs and materialized views. From 567286d1b8930b39f883d345b14ad8a5176525bb Mon Sep 17 00:00:00 2001 From: "Franz Heinzmann (Frando)" Date: Sat, 7 Dec 2019 22:37:17 +0100 Subject: [PATCH 48/85] docs --- README.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 9d17c82..60e8228 100644 --- a/README.md +++ b/README.md @@ -22,18 +22,18 @@ Register a flow. * `source` object with properties: * `open: function (flow, next)`: (optional) Handler to call on open * `pull: function (next)`: Pull new messages from the view. Should call `next` with either nothing or an object that looks like this: - ```javascript - { - messages: [], // array of messages - finished: true, // if set to false, signal that more messages are pending - onindexed: function (cb) {} // will be called when the view finished indexing - } - ``` - The source has to track its state, so that subsequent calls to `pull()` do not return the same messages. Use the `onindexed` callback to update state. - - There are several source handlers included in kappa-core (TODO: document sources). See the tests and sources directories. - - A simple state handler that perists state in a leveldb (or in memory) is included and used by the bundled source handler. It's available for use by custom sources on `Kappa.SimpleState`. If the bundled source handlers get a `db` option passed with levelup instance, the source state will be persisted. + ```javascript + { + messages: [], // array of messages + finished: true, // if set to false, signal that more messages are pending + onindexed: function (cb) {} // will be called when the view finished indexing + } + ``` + The source has to track its state, so that subsequent calls to `pull()` do not return the same messages. Use the `onindexed` callback to update state. + + There are several source handlers included in kappa-core (TODO: document sources). See the tests and sources directories. + + A simple state handler that perists state in a leveldb (or in memory) is included and used by the bundled source handler. It's available for use by custom sources on `Kappa.SimpleState`. If the bundled source handlers get a `db` option passed with levelup instance, the source state will be persisted. * `reset: function (cb)`: Reset internal state (next pull should start at the beginning). This handler is called by the kappa-core if the view's `version` property increases and thus wants to restart indexing. * `storeVersion: function (version, cb)`: Store the flow version somewhere From 7a5e14d999de9c87b98402e39ca9ccb044d8d7ba Mon Sep 17 00:00:00 2001 From: "Franz Heinzmann (Frando)" Date: Sat, 7 Dec 2019 22:44:49 +0100 Subject: [PATCH 49/85] docs --- README.md | 37 +++++++++++++++++++++++++------------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 60e8228..0235749 100644 --- a/README.md +++ b/README.md @@ -64,23 +64,36 @@ Resume processing of all flows ## Sources -* hypercore +#### [hypercore](https://github.com/mafintosh/hypercore) - `const createHypercoreSource = require('kappa-core/sources/hypercore')` - `createHypercoreSource({ feed, db })` - where `feed` is a hypercore instance and `db` is a levelup instance (for persisting state) +```javascript +const createHypercoreSource = require('kappa-core/sources/hypercore') +createHypercoreSource({ feed, db }) +``` + +where `feed` is a hypercore instance and `db` is a levelup instance (for persisting state) + +#### [multifeed](https://github.com/kappa-core/multifeed) + +```javascript +const createMultifeedSource = require('kappa-core/sources/multifeed') +createHypercoreSource({ feeds, db }) +``` + +where `feeds` is a multifeed instance and `db` is a levelup instance (for persisting state) -* multifeed +This source exposes an API method `feed (key)` that return a feed by key from the underlying multifeed. - `const createMultifeedSource = require('kappa-core/sources/multifeed')` - `createHypercoreSource({ feeds, db })` - where `feeds` is a multifeed instance and `db` is a levelup instance (for persisting state) +#### [corestore](https://github.com/andrewosh/corestore) + +```javascript +const createCorestoreSource = require('kappa-core/sources/corestore') +createHypercoreSource({ store, db }) +``` -* corestore +where `store` is a corestore instance and `db` is a levelup instance (for persisting state) - `const createCorestoreSource = require('kappa-core/sources/corestore')` - `createHypercoreSource({ store, db })` - where `store` is a corestore instance and `db` is a levelup instance (for persisting state) +This source exposes an API method `feed (key)` that return a feed by key from the underlying corestore. --- From fea70bca653c5686d926ab7e9e6d15448cec8820 Mon Sep 17 00:00:00 2001 From: "Franz Heinzmann (Frando)" Date: Sat, 7 Dec 2019 22:46:09 +0100 Subject: [PATCH 50/85] docs --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 0235749..e136be6 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ Resume processing of all flows ```javascript const createHypercoreSource = require('kappa-core/sources/hypercore') -createHypercoreSource({ feed, db }) +const source = createHypercoreSource({ feed, db }) ``` where `feed` is a hypercore instance and `db` is a levelup instance (for persisting state) @@ -77,7 +77,7 @@ where `feed` is a hypercore instance and `db` is a levelup instance (for persist ```javascript const createMultifeedSource = require('kappa-core/sources/multifeed') -createHypercoreSource({ feeds, db }) +const source = createMultifeedSource({ feeds, db }) ``` where `feeds` is a multifeed instance and `db` is a levelup instance (for persisting state) @@ -88,7 +88,7 @@ This source exposes an API method `feed (key)` that return a feed by key from th ```javascript const createCorestoreSource = require('kappa-core/sources/corestore') -createHypercoreSource({ store, db }) +const source = createCorestoreSource({ store, db }) ``` where `store` is a corestore instance and `db` is a levelup instance (for persisting state) From de37cfa921606aea5a7d8b4d649b7c3ed24ae619 Mon Sep 17 00:00:00 2001 From: "Franz Heinzmann (Frando)" Date: Fri, 13 Dec 2019 11:54:24 +0100 Subject: [PATCH 51/85] fix: storeversion only after open --- kappa.js | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/kappa.js b/kappa.js index 773cd1d..c1f804c 100644 --- a/kappa.js +++ b/kappa.js @@ -117,25 +117,26 @@ class Flow extends EventEmitter { _open (cb = noop) { if (this._opened) return cb() const self = this - let pending = 1 - if (this._view.open) ++pending && this._view.open(this, done) - if (this._source.open) ++pending && this._source.open(this, done) - - if (this._source.fetchVersion) { - pending++ - this._source.fetchVersion((err, version) => { - if (err) return done() - if (!version) return this._source.storeVersion(this.version, done) - if (version !== this.version) { - this.reset(() => this._source.storeVersion(this.version, done)) - } else done() - }) - } - done() + let pending = 1 + if (this._view.open) ++pending && this._view.open(this, onopen) + if (this._source.open) ++pending && this._source.open(this, onopen) + onopen() - function done () { + function onopen () { if (--pending !== 0) return + if (self._source.fetchVersion) { + self._source.fetchVersion((err, version) => { + if (err) return ondone() + if (!version) return self._source.storeVersion(self.version, ondone) + if (version !== self.version) { + self.reset(() => self._source.storeVersion(self.version, ondone)) + } else ondone() + }) + } else ondone() + } + + function ondone () { self._opened = true self._run() cb() From b9030745361334a38a3563e30bc8a2e253dcc338 Mon Sep 17 00:00:00 2001 From: "Franz Heinzmann (Frando)" Date: Fri, 13 Dec 2019 21:57:31 +0100 Subject: [PATCH 52/85] Use pipeline helper --- kappa.js | 33 +++++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/kappa.js b/kappa.js index c1f804c..348092a 100644 --- a/kappa.js +++ b/kappa.js @@ -106,6 +106,13 @@ class Flow extends EventEmitter { delete source.api } + // Create the list of funtions through which messages run between pull and map. + this._transform = new Pipeline() + if (this._source.transform) this._transform.push(this._source.transform.bind(this._source)) + if (this.opts.transform) this._transform.push(this.opts.transform) + if (this._view.transform) this._transform.push(this._view.transform.bind(this._view)) + if (this._view.filter) this._transform.push(this._view.filter.bind(this._view)) + this._opened = false this.open = thunky(this._open.bind(this)) } @@ -198,14 +205,8 @@ class Flow extends EventEmitter { const { messages = [], finished, onindexed } = result - let steps = [ - self._source.transform, - self.opts.transform, - self._view.transform, - self._view.filter - ] - - runThrough(messages, steps, messages => { + // TODO: Handle timeout / error? + self._transform.run(messages, messages => { if (!messages.length) return close(null, { messages, finished, onindexed }) self._view.map(messages, () => { close(null, { messages, finished, onindexed }) @@ -236,11 +237,27 @@ class Flow extends EventEmitter { } } +// Utils + function bindFn (value, ...binds) { if (typeof value === 'function') value = value.bind(...binds) return value } +class Pipeline { + constructor () { + this.fns = [] + } + + push (fn) { + this.fns.push(fn) + } + + run (messages, final) { + runThrough(messages, this.fns, final) + } +} + function runThrough (state, fns, final) { fns = fns.filter(f => f) next(state) From b1a6bbca502d0bf96733eb60d43b7c075d212375 Mon Sep 17 00:00:00 2001 From: "Franz Heinzmann (Frando)" Date: Fri, 13 Dec 2019 22:51:35 +0100 Subject: [PATCH 53/85] add close logic --- kappa.js | 32 ++++++++++++++++++++++++++++++++ test/basic.js | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+) diff --git a/kappa.js b/kappa.js index 348092a..6229e21 100644 --- a/kappa.js +++ b/kappa.js @@ -74,6 +74,17 @@ module.exports = class Kappa extends EventEmitter { } }) } + + close (cb) { + let flows = Object.values(this.flows) + let pending = flows.length + if (!pending) done() + // TODO: Propagate errors? + flows.forEach(flow => flow.close(done)) + function done () { + if (--pending === 0) cb() + } + } } class Flow extends EventEmitter { @@ -150,6 +161,26 @@ class Flow extends EventEmitter { } } + close (cb) { + const self = this + this.pause() + this._closing = true + if (this.status === Status.Running) this.once('ready', close) + else close() + function close () { + let pending = 1 + if (self._source.close) ++pending && self._source.close(cb) + if (self._view.close) ++pending && self._view.close(cb) + done() + function done () { + if (--pending !== 0) return + self._closing = false + self._opened = false + cb() + } + } + } + ready (cb) { if (!this._opened) return this.open(() => this.ready(cb)) if (this.status === Status.Ready) process.nextTick(cb) @@ -227,6 +258,7 @@ class Flow extends EventEmitter { function finish (finished) { self.status = Status.Ready + if (self._closing) return self.emit('ready') if (self.incomingUpdate || !finished) { self.incomingUpdate = false process.nextTick(self._run.bind(self)) diff --git a/test/basic.js b/test/basic.js index 1badccd..c602f17 100644 --- a/test/basic.js +++ b/test/basic.js @@ -53,6 +53,41 @@ tape('reset', t => { ]) }) +tape('open close', t => { + t.plan(5) + const kappa = new Kappa() + let i = 0 + kappa.use('foo', { + pull (next) { + t.pass('pull') + return next({ + messages: [++i, ++i], + finished: false, + onindexed (cb) { + t.pass('onindexed') + cb() + } + }) + }, + open (flow, cb) { + t.pass('open') + cb() + }, + close (cb) { + t.pass('close') + cb() + } + }, createSimpleView()) + + runAll([ + cb => kappa.close(cb), + cb => { + t.pass('closed!') + cb() + } + ]) +}) + function createSimpleView () { let res = [] let clears = 0 From b033e32f15b08c43f4dc5c0f2be871033e56a038 Mon Sep 17 00:00:00 2001 From: "Franz Heinzmann (Frando)" Date: Fri, 10 Jan 2020 16:04:15 +0100 Subject: [PATCH 54/85] if source has ready method await it in flow.ready --- kappa.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/kappa.js b/kappa.js index 6229e21..35e4101 100644 --- a/kappa.js +++ b/kappa.js @@ -181,10 +181,17 @@ class Flow extends EventEmitter { } } - ready (cb) { + ready (cb, waitForSource) { + const self = this if (!this._opened) return this.open(() => this.ready(cb)) - if (this.status === Status.Ready) process.nextTick(cb) - else this.once('ready', cb) + + if (this.source.ready) this.source.ready(onsourceready) + else onsourceready() + + function onsourceready () { + if (self.status === Status.Ready) process.nextTick(cb) + else self.once('ready', cb) + } } pause () { From 15777919bf820c7eaac6b76d1947cce93f922dac Mon Sep 17 00:00:00 2001 From: "Franz Heinzmann (Frando)" Date: Fri, 10 Jan 2020 16:13:54 +0100 Subject: [PATCH 55/85] fix onsourceready --- kappa.js | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/kappa.js b/kappa.js index 35e4101..54d7b02 100644 --- a/kappa.js +++ b/kappa.js @@ -61,18 +61,15 @@ module.exports = class Kappa extends EventEmitter { if (typeof names === 'string') names = [names] if (!names) names = Object.keys(this.flows) - // wait a tick - process.nextTick(() => { - let pending = names.length - for (const name of names) { - const flow = this.flows[name] - if (!flow) return cb(new Error('Unknown flow: ' + name)) - flow.ready(done) - } - function done () { - if (--pending === 0) cb() - } - }) + let pending = names.length + for (const name of names) { + const flow = this.flows[name] + if (!flow) return cb(new Error('Unknown flow: ' + name)) + flow.ready(done) + } + function done () { + if (--pending === 0) cb() + } } close (cb) { @@ -185,12 +182,16 @@ class Flow extends EventEmitter { const self = this if (!this._opened) return this.open(() => this.ready(cb)) - if (this.source.ready) this.source.ready(onsourceready) - else onsourceready() + setImmediate(() => { + if (this.source.ready) this.source.ready(onsourceready) + else onsourceready() + }) function onsourceready () { - if (self.status === Status.Ready) process.nextTick(cb) - else self.once('ready', cb) + process.nextTick(() => { + if (self.status === Status.Ready) process.nextTick(cb) + else self.once('ready', cb) + }) } } From 721a9e4c56ffa2fd49c5ab5501d4bd88f2c52928 Mon Sep 17 00:00:00 2001 From: "Franz Heinzmann (Frando)" Date: Sat, 22 Feb 2020 21:07:18 +0100 Subject: [PATCH 56/85] fix open logic --- kappa.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/kappa.js b/kappa.js index 54d7b02..0bf1219 100644 --- a/kappa.js +++ b/kappa.js @@ -162,7 +162,7 @@ class Flow extends EventEmitter { const self = this this.pause() this._closing = true - if (this.status === Status.Running) this.once('ready', close) + if (this.status === Status.Running) return this.once('ready', close) else close() function close () { let pending = 1 @@ -202,8 +202,8 @@ class Flow extends EventEmitter { resume () { if (this.status !== Status.Paused) return this.status = Status.Ready - if (!this._opened) this.open() - else this._run() + if (!this._opened) return this.open() + this._run() } reset (cb = noop) { @@ -230,7 +230,7 @@ class Flow extends EventEmitter { _run () { const self = this - if (!this._opened) throw new Error('Flow is not opened.') + if (!this._opened) return if (this.status === Status.Running) return if (this.status === Status.Paused) return From 3c574b4af1d8fab07b8221528451fa9b93c33f96 Mon Sep 17 00:00:00 2001 From: "Franz Heinzmann (Frando)" Date: Tue, 17 Mar 2020 19:10:12 +0100 Subject: [PATCH 57/85] support ready on source directly --- kappa.js | 1 + 1 file changed, 1 insertion(+) diff --git a/kappa.js b/kappa.js index 0bf1219..ac63ce3 100644 --- a/kappa.js +++ b/kappa.js @@ -184,6 +184,7 @@ class Flow extends EventEmitter { setImmediate(() => { if (this.source.ready) this.source.ready(onsourceready) + else if (this._source.ready) this._source.ready(onsourceready) else onsourceready() }) From f87f74008dfe4b5e5685a09354d5ee2ac1e5bab0 Mon Sep 17 00:00:00 2001 From: "Franz Heinzmann (Frando)" Date: Tue, 17 Mar 2020 19:12:07 +0100 Subject: [PATCH 58/85] support opts.filter for flows --- kappa.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/kappa.js b/kappa.js index ac63ce3..487f087 100644 --- a/kappa.js +++ b/kappa.js @@ -116,10 +116,12 @@ class Flow extends EventEmitter { // Create the list of funtions through which messages run between pull and map. this._transform = new Pipeline() + if (this._source.filter) this._transform.push(this._source.filter.bind(this._source)) if (this._source.transform) this._transform.push(this._source.transform.bind(this._source)) + if (this.opts.filter) this._transform.push(this.opts.filter) if (this.opts.transform) this._transform.push(this.opts.transform) - if (this._view.transform) this._transform.push(this._view.transform.bind(this._view)) if (this._view.filter) this._transform.push(this._view.filter.bind(this._view)) + if (this._view.transform) this._transform.push(this._view.transform.bind(this._view)) this._opened = false this.open = thunky(this._open.bind(this)) @@ -240,6 +242,7 @@ class Flow extends EventEmitter { this._source.pull(onbatch) function onbatch (result) { + // console.log(self.name, 'onbatch', result) if (self.status === Status.Paused) return close() if (!result || !result.messages.length) return close(null, result) From 307c23458c7b506ed967992535f706405681abdf Mon Sep 17 00:00:00 2001 From: "Franz Heinzmann (Frando)" Date: Fri, 20 Mar 2020 23:38:05 +0100 Subject: [PATCH 59/85] missing dev deps --- package.json | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index d2340fd..805eb3b 100644 --- a/package.json +++ b/package.json @@ -21,13 +21,17 @@ }, "devDependencies": { "bitfield-db": "^1.2.2", - "corestore": "^4.0.0", + "corestore": "^5.0.0", "hypercore": "^8", - "hyperdrive": "github:mafintosh/hyperdrive#hypercore-8", - "hyperdrive-schemas": "^0.12.0", + "hypercore-crypto": "^1.0.0", + "hypercore-protocol": "^7.10.0", + "hyperdrive": "^10.8.18", + "hyperdrive-schemas": "beta", "level-mem": "^5.0.1", "multifeed": "^5", "nanoiterator": "^1.2.0", + "nanoresource": "^1.3.0", + "random-access-file": "^2.1.3", "random-access-memory": "^3.1.1", "standard": "~12.0.1", "tape": "^4.11.0" From 4eab8c6be7aaaea5cb62fc3e0af347bf3c7cf6f5 Mon Sep 17 00:00:00 2001 From: "Franz Heinzmann (Frando)" Date: Fri, 20 Mar 2020 23:38:20 +0100 Subject: [PATCH 60/85] filter empty messages --- kappa.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/kappa.js b/kappa.js index 487f087..1618f31 100644 --- a/kappa.js +++ b/kappa.js @@ -244,7 +244,9 @@ class Flow extends EventEmitter { function onbatch (result) { // console.log(self.name, 'onbatch', result) if (self.status === Status.Paused) return close() - if (!result || !result.messages.length) return close(null, result) + if (!result) return close() + if (result.messages) result.messages = result.messages.filter(m => m) + if (!result.messages.length) return close(null, result) const { messages = [], finished, onindexed } = result From f0d3def631420ff233f72223cfad9e06161ead38 Mon Sep 17 00:00:00 2001 From: "Franz Heinzmann (Frando)" Date: Tue, 28 Apr 2020 01:04:11 +0200 Subject: [PATCH 61/85] open in nextTick --- kappa.js | 7 ++++--- test/basic.js | 3 ++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/kappa.js b/kappa.js index 1618f31..fd6c804 100644 --- a/kappa.js +++ b/kappa.js @@ -32,10 +32,11 @@ module.exports = class Kappa extends EventEmitter { this.source[name] = flow.source flow.on('error', err => this.emit('error', err, flow)) - this.emit('flow', name) - - if (this.status !== Status.Paused) flow.open() + if (this.status !== Status.Paused) { + process.nextTick(() => flow.open()) + } + this.emit('flow', name) return flow } diff --git a/test/basic.js b/test/basic.js index c602f17..2c2833d 100644 --- a/test/basic.js +++ b/test/basic.js @@ -62,7 +62,7 @@ tape('open close', t => { t.pass('pull') return next({ messages: [++i, ++i], - finished: false, + finished: true, onindexed (cb) { t.pass('onindexed') cb() @@ -80,6 +80,7 @@ tape('open close', t => { }, createSimpleView()) runAll([ + cb => kappa.ready(cb), cb => kappa.close(cb), cb => { t.pass('closed!') From b03cc5b09235e9079916683d4242fc64a6547ef7 Mon Sep 17 00:00:00 2001 From: "Franz Heinzmann (Frando)" Date: Sat, 2 May 2020 15:05:53 +0200 Subject: [PATCH 62/85] first round of fixes after review --- README.md | 3 ++- kappa.js | 29 ++++++++++++++++++++++------- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index e136be6..b6cc475 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ Register a flow. * `name` (string) the name of the flow, has to be unique per kappa core * `source` object with properties: * `open: function (flow, next)`: (optional) Handler to call on open + * `close: function (next)`: (optional) Handler to call on close * `pull: function (next)`: Pull new messages from the view. Should call `next` with either nothing or an object that looks like this: ```javascript { @@ -52,7 +53,7 @@ Reset a specific flow, to restart indexing. This is equal to reopening the kappa #### `kappa.ready(names, cb)` -Call `cb` after all flows with a name in the `names` array have finished processing. If `names` is empty, all flows will be awaited. +Call `cb` exactly once, after all flows with a name in the `names` array have finished processing. If `names` is empty, all flows will be awaited. If all the flows are already ready, `cb` is called immediately. #### `kappa.pause()` diff --git a/kappa.js b/kappa.js index fd6c804..01ca53c 100644 --- a/kappa.js +++ b/kappa.js @@ -61,6 +61,9 @@ module.exports = class Kappa extends EventEmitter { if (typeof names === 'function') return this.ready(null, names) if (typeof names === 'string') names = [names] if (!names) names = Object.keys(this.flows) + if (!names.length) return cb() + + cb = once(cb) let pending = names.length for (const name of names) { @@ -74,12 +77,13 @@ module.exports = class Kappa extends EventEmitter { } close (cb) { + cb = once(cb) let flows = Object.values(this.flows) let pending = flows.length if (!pending) done() - // TODO: Propagate errors? flows.forEach(flow => flow.close(done)) - function done () { + function done (err) { + if (err) return cb(err) if (--pending === 0) cb() } } @@ -181,7 +185,7 @@ class Flow extends EventEmitter { } } - ready (cb, waitForSource) { + ready (cb) { const self = this if (!this._opened) return this.open(() => this.ready(cb)) @@ -243,7 +247,6 @@ class Flow extends EventEmitter { this._source.pull(onbatch) function onbatch (result) { - // console.log(self.name, 'onbatch', result) if (self.status === Status.Paused) return close() if (!result) return close() if (result.messages) result.messages = result.messages.filter(m => m) @@ -254,14 +257,17 @@ class Flow extends EventEmitter { // TODO: Handle timeout / error? self._transform.run(messages, messages => { if (!messages.length) return close(null, { messages, finished, onindexed }) - self._view.map(messages, () => { - close(null, { messages, finished, onindexed }) + self._view.map(messages, err => { + close(err, { messages, finished, onindexed }) }) }) } function close (err, result) { - if (err) self.emit('error', err) + if (err) { + self.emit('error', err) + return finish(false) + } if (!result) return finish(true) const { messages, finished, onindexed } = result if (messages.length && self._view.indexed) { @@ -318,3 +324,12 @@ function runThrough (state, fns, final) { } function noop () {} + +function once (fn) { + let called = false + return (...args) => { + if (called) return + called = true + fn(...args) + } +} From ba96eda5a5d5c80c5e27353c2b43da1d229cc94c Mon Sep 17 00:00:00 2001 From: "Franz Heinzmann (Frando)" Date: Sat, 2 May 2020 15:41:32 +0200 Subject: [PATCH 63/85] more fixes and doc improvements after review --- .gitignore | 2 ++ README.md | 27 +++++++++++++++------------ index.js | 2 ++ kappa.js | 2 +- 4 files changed, 20 insertions(+), 13 deletions(-) diff --git a/.gitignore b/.gitignore index d01fca5..d320cbd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ node_modules package-lock.json pnpm-lock.yaml +yarn.lock +SANDBOX diff --git a/README.md b/README.md index b6cc475..6a8eb8a 100644 --- a/README.md +++ b/README.md @@ -30,22 +30,25 @@ Register a flow. onindexed: function (cb) {} // will be called when the view finished indexing } ``` - The source has to track its state, so that subsequent calls to `pull()` do not return the same messages. Use the `onindexed` callback to update state. - - There are several source handlers included in kappa-core (TODO: document sources). See the tests and sources directories. - - A simple state handler that perists state in a leveldb (or in memory) is included and used by the bundled source handler. It's available for use by custom sources on `Kappa.SimpleState`. If the bundled source handlers get a `db` option passed with levelup instance, the source state will be persisted. - * `reset: function (cb)`: Reset internal state (next pull should start at the beginning). This handler is called by the kappa-core if the view's `version` property increases and thus wants to restart indexing. * `storeVersion: function (version, cb)`: Store the flow version somewhere * `fetchVersion: function (cb)`: Fetch the version stored with storeVersion + * `view` object with properties - * `map: function (msgs, next)` (required) - * `open: function (flow, next)` (optional) - * `filter: function (msgs, next)` (optional) - * `version: int` + * `map: function (messages, next)` (required) Called for each batch of messages. Call `next` when done indexing this batch of messages. + * `open: function (flow, next)` (optional) Callback to call on open. `flow` is the current flow, it notably has a `name` property that uniquely identifies this flow within the current Kappa core. Has to call `next` when done with opening. + * `clearIndex: function (cb)`: Delete all indexed data. This is called by the Kappa core when a complete reindex is necessary. The `map` function will receive messages from the start on afterwards. + * `version: int` The view version. If the version is increased, the Kappa core will clear and restart the indexing for this view after the next reopening of the core. + +Both `source` and `view` can have an `api` property with an object of function. The functions are exposed on `kappa.view[name]` / `kappa.source[name]`. Their `this` object refers to the flow they are part of, and their first parameter is the `kappa`. Other parameters are passed through. + +The source has to track its state, so that subsequent calls to `pull()` do not return the same messages. Use the `onindexed` callback to update state. How to track its state is up to the source implementation. + +A simple state handler that perists state in a leveldb (or in memory) is included and used by the bundled source handler. It's available for use by custom sources on `Kappa.SimpleState`. If the bundled source handlers get a `db` option passed with levelup instance, the source state will be persisted. + +There are several source handlers included in kappa-core (TODO: document sources). See the tests and sources directories. + -Both `source` and `view` can have an `api` property with an object of function. The functions are exposed on `kappa.view[name]` / `kappa.source[name]`. Their `this` object refers to the flow, and their first parameter is the `kappa`. Other parameters are passed through. #### `kappa.reset(name, cb)` @@ -53,7 +56,7 @@ Reset a specific flow, to restart indexing. This is equal to reopening the kappa #### `kappa.ready(names, cb)` -Call `cb` exactly once, after all flows with a name in the `names` array have finished processing. If `names` is empty, all flows will be awaited. If all the flows are already ready, `cb` is called immediately. +Call `cb` exactly once, after all flows with a name in the `names` array have finished processing. If `names` is empty, all flows will be awaited. This `names` is a string, the flow of this name will be awaited. If the requested flows are already ready, `cb` is called immediately. #### `kappa.pause()` diff --git a/index.js b/index.js index 95f6b4a..3ff6a60 100644 --- a/index.js +++ b/index.js @@ -1,4 +1,6 @@ const Kappa = require('./kappa') +const SimpleState = require('./sources/util/state') module.exports = Kappa module.exports.Kappa = Kappa +module.exports.SimpleState = SimpleState diff --git a/kappa.js b/kappa.js index 01ca53c..7fc5188 100644 --- a/kappa.js +++ b/kappa.js @@ -80,7 +80,7 @@ module.exports = class Kappa extends EventEmitter { cb = once(cb) let flows = Object.values(this.flows) let pending = flows.length - if (!pending) done() + if (!pending) return done() flows.forEach(flow => flow.close(done)) function done (err) { if (err) return cb(err) From 0e3298a5f3c76545619606da2ddee3d58518798f Mon Sep 17 00:00:00 2001 From: "Franz Heinzmann (Frando)" Date: Sat, 2 May 2020 15:41:43 +0200 Subject: [PATCH 64/85] remove filter option for sources and views --- kappa.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/kappa.js b/kappa.js index 7fc5188..d64b59b 100644 --- a/kappa.js +++ b/kappa.js @@ -121,11 +121,8 @@ class Flow extends EventEmitter { // Create the list of funtions through which messages run between pull and map. this._transform = new Pipeline() - if (this._source.filter) this._transform.push(this._source.filter.bind(this._source)) if (this._source.transform) this._transform.push(this._source.transform.bind(this._source)) - if (this.opts.filter) this._transform.push(this.opts.filter) if (this.opts.transform) this._transform.push(this.opts.transform) - if (this._view.filter) this._transform.push(this._view.filter.bind(this._view)) if (this._view.transform) this._transform.push(this._view.transform.bind(this._view)) this._opened = false From 20eb000ff2db75264f64fae07d96ec8fc5f7c002 Mon Sep 17 00:00:00 2001 From: "Franz Heinzmann (Frando)" Date: Sat, 2 May 2020 15:45:08 +0200 Subject: [PATCH 65/85] optimization: ensure good version value, dont remove api props --- kappa.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/kappa.js b/kappa.js index d64b59b..b782ba0 100644 --- a/kappa.js +++ b/kappa.js @@ -96,6 +96,8 @@ class Flow extends EventEmitter { this.opts = opts this.name = name + if (!view.version) view.version = 1 + this._view = view this._source = source @@ -110,13 +112,11 @@ class Flow extends EventEmitter { for (let [key, value] of Object.entries(view.api)) { this.view[key] = bindFn(value, this, this.context) } - delete view.api } if (source.api) { for (let [key, value] of Object.entries(source.api)) { this.source[key] = bindFn(value, this, this.context) } - delete source.api } // Create the list of funtions through which messages run between pull and map. @@ -130,7 +130,7 @@ class Flow extends EventEmitter { } get version () { - return this._view.version || 1 + return this._view.version } _open (cb = noop) { From 3e5ab174c55005d1357a1c00f777641abe5bfcae Mon Sep 17 00:00:00 2001 From: "Franz Heinzmann (Frando)" Date: Sat, 2 May 2020 16:11:43 +0200 Subject: [PATCH 66/85] support errors on open --- kappa.js | 53 +++++++++++++++++++++++++++++++-------------------- test/basic.js | 37 +++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 21 deletions(-) diff --git a/kappa.js b/kappa.js index b782ba0..b49c90d 100644 --- a/kappa.js +++ b/kappa.js @@ -33,7 +33,9 @@ module.exports = class Kappa extends EventEmitter { flow.on('error', err => this.emit('error', err, flow)) if (this.status !== Status.Paused) { - process.nextTick(() => flow.open()) + process.nextTick(() => flow.open(err => { + if (err) this.emit('error', err) + })) } this.emit('flow', name) @@ -125,7 +127,7 @@ class Flow extends EventEmitter { if (this.opts.transform) this._transform.push(this.opts.transform) if (this._view.transform) this._transform.push(this._view.transform.bind(this._view)) - this._opened = false + this.opened = false this.open = thunky(this._open.bind(this)) } @@ -134,29 +136,38 @@ class Flow extends EventEmitter { } _open (cb = noop) { - if (this._opened) return cb() + if (this.opened) return cb() const self = this - + let done = false let pending = 1 if (this._view.open) ++pending && this._view.open(this, onopen) if (this._source.open) ++pending && this._source.open(this, onopen) onopen() - function onopen () { + function onopen (err) { + if (err) return ondone(err) if (--pending !== 0) return - if (self._source.fetchVersion) { - self._source.fetchVersion((err, version) => { - if (err) return ondone() - if (!version) return self._source.storeVersion(self.version, ondone) - if (version !== self.version) { - self.reset(() => self._source.storeVersion(self.version, ondone)) - } else ondone() - }) - } else ondone() + if (!self._source.fetchVersion) return ondone() + + self._source.fetchVersion((err, version) => { + if (err) return ondone(err) + if (!version) { + self._source.storeVersion(self.version, ondone) + } else if (version !== self.version) { + self.reset(() => { + self._source.storeVersion(self.version, ondone) + }) + } else { + ondone() + } + }) } - function ondone () { - self._opened = true + function ondone (err) { + if (done) return + done = true + if (err) return cb(err) + self.opened = true self._run() cb() } @@ -176,7 +187,7 @@ class Flow extends EventEmitter { function done () { if (--pending !== 0) return self._closing = false - self._opened = false + self.opened = false cb() } } @@ -184,7 +195,7 @@ class Flow extends EventEmitter { ready (cb) { const self = this - if (!this._opened) return this.open(() => this.ready(cb)) + if (!this.opened) return this.open(() => this.ready(cb)) setImmediate(() => { if (this.source.ready) this.source.ready(onsourceready) @@ -207,7 +218,7 @@ class Flow extends EventEmitter { resume () { if (this.status !== Status.Paused) return this.status = Status.Ready - if (!this._opened) return this.open() + if (!this.opened) return this.open() this._run() } @@ -228,14 +239,14 @@ class Flow extends EventEmitter { } update () { - if (!this._opened) return + if (!this.opened) return this.incomingUpdate = true process.nextTick(this._run.bind(this)) } _run () { const self = this - if (!this._opened) return + if (!this.opened) return if (this.status === Status.Running) return if (this.status === Status.Paused) return diff --git a/test/basic.js b/test/basic.js index 2c2833d..e4a2eef 100644 --- a/test/basic.js +++ b/test/basic.js @@ -89,6 +89,43 @@ tape('open close', t => { ]) }) +tape('open error', t => { + const kappa = new Kappa() + kappa.use('foo', { + open (flow, cb) { + cb(new Error('open error')) + }, + pull (next) { next() } + }, createSimpleView()) + kappa.use('bar', { + open (flow, cb) { + cb() + }, + pull (next) { next() } + }, createSimpleView()) + kappa.on('error', err => { + t.equal(err.message, 'open error') + t.equal(kappa.flows.foo.opened, false) + t.equal(kappa.flows.bar.opened, true) + t.end() + }) +}) + +tape('fetch version error', t => { + const kappa = new Kappa() + kappa.use('foo', { + fetchVersion (cb) { + cb(new Error('fetch version error')) + }, + pull (next) { next() } + }, createSimpleView()) + kappa.on('error', err => { + t.equal(err.message, 'fetch version error') + t.equal(kappa.flows.foo.opened, false) + t.end() + }) +}) + function createSimpleView () { let res = [] let clears = 0 From 040fdae022602962bbfdbd1ebf77328e7af903e1 Mon Sep 17 00:00:00 2001 From: "Franz Heinzmann (Frando)" Date: Sat, 2 May 2020 16:17:54 +0200 Subject: [PATCH 67/85] rename view.clearIndex to view.reset --- README.md | 8 ++++---- kappa.js | 7 ++++++- test/basic.js | 2 +- test/hypercore.js | 2 +- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 6a8eb8a..3097ce8 100644 --- a/README.md +++ b/README.md @@ -30,14 +30,14 @@ Register a flow. onindexed: function (cb) {} // will be called when the view finished indexing } ``` - * `reset: function (cb)`: Reset internal state (next pull should start at the beginning). This handler is called by the kappa-core if the view's `version` property increases and thus wants to restart indexing. - * `storeVersion: function (version, cb)`: Store the flow version somewhere - * `fetchVersion: function (cb)`: Fetch the version stored with storeVersion + * `reset: function (cb)`: Delete internal state. This is called when a full reindex is necessary. This means that the next pull ought to start at the beginning. + * `storeVersion: function (version, cb)`: Store the flow version number somewhere. + * `fetchVersion: function (cb)`: Fetch the version stored with `storeVersion` * `view` object with properties * `map: function (messages, next)` (required) Called for each batch of messages. Call `next` when done indexing this batch of messages. * `open: function (flow, next)` (optional) Callback to call on open. `flow` is the current flow, it notably has a `name` property that uniquely identifies this flow within the current Kappa core. Has to call `next` when done with opening. - * `clearIndex: function (cb)`: Delete all indexed data. This is called by the Kappa core when a complete reindex is necessary. The `map` function will receive messages from the start on afterwards. + * `reset: function (cb)`: Delete all indexed data. This is called by the Kappa core when a complete reindex is necessary. The `map` function will receive messages from the start on afterwards. * `version: int` The view version. If the version is increased, the Kappa core will clear and restart the indexing for this view after the next reopening of the core. Both `source` and `view` can have an `api` property with an object of function. The functions are exposed on `kappa.view[name]` / `kappa.source[name]`. Their `this` object refers to the flow they are part of, and their first parameter is the `kappa`. Other parameters are passed through. diff --git a/kappa.js b/kappa.js index b49c90d..85f66e6 100644 --- a/kappa.js +++ b/kappa.js @@ -100,6 +100,11 @@ class Flow extends EventEmitter { if (!view.version) view.version = 1 + // TODO: Backward-compatibility only. Remove. + if (view.clearIndex && !view.reset) { + view.reset = view.clearIndex.bind(view) + } + this._view = view this._source = source @@ -227,7 +232,7 @@ class Flow extends EventEmitter { this.pause() let pending = 1 process.nextTick(() => { - if (this._view.clearIndex) ++pending && this._view.clearIndex(done) + if (this._view.reset) ++pending && this._view.reset(done) if (this._source.reset) ++pending && this._source.reset(done) done() }) diff --git a/test/basic.js b/test/basic.js index e4a2eef..0087d72 100644 --- a/test/basic.js +++ b/test/basic.js @@ -134,7 +134,7 @@ function createSimpleView () { res = res.concat(msgs) next() }, - clearIndex (cb) { + reset (cb) { clears = clears + 1 res = [] cb() diff --git a/test/hypercore.js b/test/hypercore.js index f469e7d..23c493f 100644 --- a/test/hypercore.js +++ b/test/hypercore.js @@ -113,7 +113,7 @@ function makeSimpleView (db, version) { }) }, version, - clearIndex (cb) { + reset (cb) { clears = clears + 1 db.put('msgs', JSON.stringify([]), cb) }, From bc39cf3995c5f0c3a644e3b37a9b7efc1e6629e6 Mon Sep 17 00:00:00 2001 From: "Franz Heinzmann (Frando)" Date: Sat, 2 May 2020 16:21:00 +0200 Subject: [PATCH 68/85] docs formatting --- README.md | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 3097ce8..47da09f 100644 --- a/README.md +++ b/README.md @@ -20,9 +20,9 @@ Register a flow. * `name` (string) the name of the flow, has to be unique per kappa core * `source` object with properties: - * `open: function (flow, next)`: (optional) Handler to call on open - * `close: function (next)`: (optional) Handler to call on close - * `pull: function (next)`: Pull new messages from the view. Should call `next` with either nothing or an object that looks like this: + * `open: function (flow, cb)`: (optional) Handler to call on open. + * `close: function (cb)`: (optional) Handler to call on close. + * `pull: function (next)`: Handler to pull new messages from the view. Should call `next` with either nothing or an object that looks like this: ```javascript { messages: [], // array of messages @@ -30,14 +30,15 @@ Register a flow. onindexed: function (cb) {} // will be called when the view finished indexing } ``` - * `reset: function (cb)`: Delete internal state. This is called when a full reindex is necessary. This means that the next pull ought to start at the beginning. - * `storeVersion: function (version, cb)`: Store the flow version number somewhere. - * `fetchVersion: function (cb)`: Fetch the version stored with `storeVersion` - -* `view` object with properties - * `map: function (messages, next)` (required) Called for each batch of messages. Call `next` when done indexing this batch of messages. - * `open: function (flow, next)` (optional) Callback to call on open. `flow` is the current flow, it notably has a `name` property that uniquely identifies this flow within the current Kappa core. Has to call `next` when done with opening. - * `reset: function (cb)`: Delete all indexed data. This is called by the Kappa core when a complete reindex is necessary. The `map` function will receive messages from the start on afterwards. + * `reset: function (cb)`: Handler to reset internal state. This is called when a full reindex is necessary. This means that the next pull ought to start at the beginning. + * `storeVersion: function (version, cb)`: Handler to store the flow version number. + * `fetchVersion: function (cb)`: Handler to fetch the version stored with `storeVersion`. + +* `view` object with properties: + * `map: function (messages, next)` (required) Handler for each batch of messages. Call `next` when done indexing this batch of messages. + * `open: function (flow, cb)` (optional) Handler to call on open. `flow` is the current flow, it notably has a `name` property that uniquely identifies this flow within the current Kappa core. Call `next` when done with opening. + * `close: function (cb)`: (optional) Handler to call on close. + * `reset: function (cb)`: Handler to delete all indexed data. This is called by the Kappa core when a complete reindex is necessary. The `map` function will receive messages from the start on afterwards. * `version: int` The view version. If the version is increased, the Kappa core will clear and restart the indexing for this view after the next reopening of the core. Both `source` and `view` can have an `api` property with an object of function. The functions are exposed on `kappa.view[name]` / `kappa.source[name]`. Their `this` object refers to the flow they are part of, and their first parameter is the `kappa`. Other parameters are passed through. @@ -48,8 +49,6 @@ A simple state handler that perists state in a leveldb (or in memory) is include There are several source handlers included in kappa-core (TODO: document sources). See the tests and sources directories. - - #### `kappa.reset(name, cb)` Reset a specific flow, to restart indexing. This is equal to reopening the kappa-core with a changed view version for this flow. From eaa5a0aae3c29522bc64b0c38f4be97775cb6a15 Mon Sep 17 00:00:00 2001 From: "Franz Heinzmann (Frando)" Date: Sat, 2 May 2020 16:56:00 +0200 Subject: [PATCH 69/85] Docs improvements --- README.md | 77 +++++++++++++++++++++++++++++++++++--------- sources/hypercore.js | 4 --- 2 files changed, 62 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 47da09f..07e8ebe 100644 --- a/README.md +++ b/README.md @@ -20,9 +20,9 @@ Register a flow. * `name` (string) the name of the flow, has to be unique per kappa core * `source` object with properties: - * `open: function (flow, cb)`: (optional) Handler to call on open. - * `close: function (cb)`: (optional) Handler to call on close. - * `pull: function (next)`: Handler to pull new messages from the view. Should call `next` with either nothing or an object that looks like this: + * `open: function (flow, cb)` *(optional)* Handler to call on open. `flow` is the current flow object (see below for docs). Call `cb` when done with opening. + * `close: function (cb)`: *(optional)* Handler to call on close. Has to call `cb`. + * `pull: function (next)`: **(required)** Handler to pull new messages from the view. Should call `next` with either nothing or an object that looks like this: ```javascript { messages: [], // array of messages @@ -30,22 +30,21 @@ Register a flow. onindexed: function (cb) {} // will be called when the view finished indexing } ``` - * `reset: function (cb)`: Handler to reset internal state. This is called when a full reindex is necessary. This means that the next pull ought to start at the beginning. - * `storeVersion: function (version, cb)`: Handler to store the flow version number. - * `fetchVersion: function (cb)`: Handler to fetch the version stored with `storeVersion`. + * `reset: function (cb)`: **(required)** Handler to reset internal state. This is called when a full reindex is necessary. This means that the next pull ought to start at the beginning. + * `storeVersion: function (version, cb)`: **(required)** Handler to store the flow version number. + * `fetchVersion: function (cb)`: **(required)** Handler to fetch the version stored with `storeVersion`. + * See the `SimpleState` docs below how to easily implement the `reset`, `storeVersion` and `fetchVersion` methods. * `view` object with properties: - * `map: function (messages, next)` (required) Handler for each batch of messages. Call `next` when done indexing this batch of messages. - * `open: function (flow, cb)` (optional) Handler to call on open. `flow` is the current flow, it notably has a `name` property that uniquely identifies this flow within the current Kappa core. Call `next` when done with opening. - * `close: function (cb)`: (optional) Handler to call on close. - * `reset: function (cb)`: Handler to delete all indexed data. This is called by the Kappa core when a complete reindex is necessary. The `map` function will receive messages from the start on afterwards. - * `version: int` The view version. If the version is increased, the Kappa core will clear and restart the indexing for this view after the next reopening of the core. + * `open: function (flow, cb)` *(optional)* Handler to call on open. `flow` is the current flow object (see below for docs). Call `cb` when done with opening. + * `close: function (cb)`: *(optional)* Handler to call on close. Has to call `cb`. + * `map: function (messages, next)` **(required)** Handler for each batch of messages. Call `next` when done indexing this batch of messages. + * `reset: function (cb)`: **(required)** Handler to delete all indexed data. This is called by the Kappa core when a complete reindex is necessary. The `map` function will receive messages from the start on afterwards. + * `version: int` The view version. If the version is increased, the Kappa core will clear and restart the indexing for this view after the next reopening of the core. Defaults to `1`. -Both `source` and `view` can have an `api` property with an object of function. The functions are exposed on `kappa.view[name]` / `kappa.source[name]`. Their `this` object refers to the flow they are part of, and their first parameter is the `kappa`. Other parameters are passed through. +Both `source` and `view` can have an `api` property with an object of functions. The functions are exposed on `kappa.view[name]` / `kappa.source[name]`. Their `this` object refers to the flow they are part of, and their first parameter is the `kappa`. Other parameters are passed through. -The source has to track its state, so that subsequent calls to `pull()` do not return the same messages. Use the `onindexed` callback to update state. How to track its state is up to the source implementation. - -A simple state handler that perists state in a leveldb (or in memory) is included and used by the bundled source handler. It's available for use by custom sources on `Kappa.SimpleState`. If the bundled source handlers get a `db` option passed with levelup instance, the source state will be persisted. +The source has to track its state, so that subsequent calls to `pull()` do not return the same messages. Use the `onindexed` callback to update state. How to track its state is up to the source implementation. kappa-core provides a `SimpleState` helper to simplify this, see its documentation below. There are several source handlers included in kappa-core (TODO: document sources). See the tests and sources directories. @@ -65,6 +64,54 @@ Pause processing of all flows Resume processing of all flows +## Flow + +When calling `kappa.use()` a new *Flow* is created. A Flow is the combination of a source and a view - where the data flows from the source into the view. The `Flow` object is passed to sources and views in their `open` handler. It has this public API: + +* `flow.name`: (string) A name that uniquely identifies this flow within the Kappa core. +* `flow.update()`: Signal to the flow that the source has new data available. Youwant to call this from a source when the source has new data. If the Kappa core is not paused, this will cause the `pull` handler to be called. +* `flow.ready(cb)`: Calls `cb` (with no arguments) when this flow has finished processing all messages. `cb` is called immediately if the flow is already finished. +* `flow.view`: Object with the view's API functions +* `flow.source`: Object with the source's API functions + +## SimpleState + +`kappa-core` exports a `SimpleState` class that can be used by sources for a simple state handling. It persists state either in-memory, and supports a [LevelDB](https://github.com/Level/level) (or compatible) option for persistence. + +Example: + +```javascript +const { KappaCore, SimpleState } = require('kappa-core') +function createSource (opts) { + const state = new SimpleState({ db: opts.db }) + return { + pull (next) { + // get your current state + state.get((err, state) => { + if (err) return next() + // fetch messages from your data source + fetchMessages(state, ({ messages, finished, nextState }) => { + // call next with an onindexed handler + next({ + messages, + finished, + onindexed (cb) { + // store the new state + state.put(nextState, cb) + } + }) + }) + }) + }, + fetchVersion: state.fetchVersion, + storeVersion: state.storeVersion, + reset (cb) { + state.put('', cb) + } + } +} +``` + ## Sources #### [hypercore](https://github.com/mafintosh/hypercore) diff --git a/sources/hypercore.js b/sources/hypercore.js index 56c4dc5..37f42e2 100644 --- a/sources/hypercore.js +++ b/sources/hypercore.js @@ -24,10 +24,6 @@ class HypercoreSource { this.state.storeVersion(version, cb) } - reset (cb) { - this.state.set(0, cb) - } - get name () { return this.feed.key.toString('hex') } From 86472bd32a85e53aa3b5043856b9ae63a8b5144b Mon Sep 17 00:00:00 2001 From: "Franz Heinzmann (Frando)" Date: Sat, 2 May 2020 17:09:51 +0200 Subject: [PATCH 70/85] more fixes from nettle's review --- kappa.js | 7 +++---- package.json | 2 +- sources/multifeed.js | 3 +-- sources/util/merge.js | 4 ++-- sources/util/state.js | 8 ++++++-- test/multifeed.js | 8 -------- 6 files changed, 13 insertions(+), 19 deletions(-) diff --git a/kappa.js b/kappa.js index 85f66e6..1ea3791 100644 --- a/kappa.js +++ b/kappa.js @@ -64,7 +64,6 @@ module.exports = class Kappa extends EventEmitter { if (typeof names === 'string') names = [names] if (!names) names = Object.keys(this.flows) if (!names.length) return cb() - cb = once(cb) let pending = names.length @@ -186,8 +185,8 @@ class Flow extends EventEmitter { else close() function close () { let pending = 1 - if (self._source.close) ++pending && self._source.close(cb) - if (self._view.close) ++pending && self._view.close(cb) + if (self._source.close) ++pending && self._source.close(done) + if (self._view.close) ++pending && self._view.close(done) done() function done () { if (--pending !== 0) return @@ -267,9 +266,9 @@ class Flow extends EventEmitter { const { messages = [], finished, onindexed } = result - // TODO: Handle timeout / error? self._transform.run(messages, messages => { if (!messages.length) return close(null, { messages, finished, onindexed }) + // TODO: Handle timeout? self._view.map(messages, err => { close(err, { messages, finished, onindexed }) }) diff --git a/package.json b/package.json index 805eb3b..0d28536 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "kappa-core", "description": "Minimal peer-to-peer database, based on kappa architecture.", "author": "Stephen Whitmore , Franz Heinzmann ", - "version": "7.0.0-alpha1", + "version": "4.0.1", "repository": { "url": "git://github.com/noffle/kappa-core.git" }, diff --git a/sources/multifeed.js b/sources/multifeed.js index aa71dee..d2aeae7 100644 --- a/sources/multifeed.js +++ b/sources/multifeed.js @@ -30,7 +30,7 @@ module.exports = function multifeedSource (opts) { } } - function onfeed (flow, feed, cb) { + function onfeed (flow, feed) { const source = hypercoreSource({ feed, state @@ -38,7 +38,6 @@ module.exports = function multifeedSource (opts) { sources.push(source) source.open(flow, () => { flow.update() - if (cb) cb() }) } } diff --git a/sources/util/merge.js b/sources/util/merge.js index db80cc7..40e672c 100644 --- a/sources/util/merge.js +++ b/sources/util/merge.js @@ -1,5 +1,5 @@ exports.mergePull = function (sources, next) { - if (!sources.length) return next() + if (!sources.length) return process.nextTick(next) let results = [] let pending = sources.length sources.forEach(source => source.pull(onresult)) @@ -12,7 +12,7 @@ exports.mergePull = function (sources, next) { let messages = [] let finished = true for (let result of results) { - if (result.messages) messages = messages.concat(result.messages) + if (result.messages) Array.prototype.push.apply(messages, result.messages) if (!result.finished) finished = false } next({ diff --git a/sources/util/state.js b/sources/util/state.js index b828ce1..b141555 100644 --- a/sources/util/state.js +++ b/sources/util/state.js @@ -18,6 +18,9 @@ module.exports = class SimpleState { }) } + reset (cb) { + } + get (name, cb) { if (!cb) return this.get('', name) const key = this._STATE + name @@ -62,7 +65,7 @@ class FakeDB { put (key, value, cb) { this.state[key] = value - cb() + process.nextTick(cb) } get (key, cb) { @@ -70,9 +73,10 @@ class FakeDB { const err = new Error('Key not found') err.type = 'NotFoundError' err.notFound = true + process.nextTick(cb, err) cb(err) } else { - cb(null, this.state[key]) + process.nextTick(cb, null, this.state[key]) } } } diff --git a/test/multifeed.js b/test/multifeed.js index 8ec0e6e..d745dfe 100644 --- a/test/multifeed.js +++ b/test/multifeed.js @@ -61,11 +61,3 @@ function createSumView () { } return sumview } - -// function replicate (a, b, opts, cb) { -// if (typeof opts === 'function') return replicate(a, b, null, cb) -// if (!opts) opts = { live: true } -// const stream = a.replicate(opts) -// stream.pipe(b.replicate(opts)).pipe(stream) -// setImmediate(cb) -// } From 2b60ba3448ca68c97178fbd20f71e2337a913adb Mon Sep 17 00:00:00 2001 From: "Franz Heinzmann (Frando)" Date: Sat, 2 May 2020 17:12:25 +0200 Subject: [PATCH 71/85] remove outdated test --- test/stacked-view.js | 152 ------------------------------------------- 1 file changed, 152 deletions(-) delete mode 100644 test/stacked-view.js diff --git a/test/stacked-view.js b/test/stacked-view.js deleted file mode 100644 index da3977a..0000000 --- a/test/stacked-view.js +++ /dev/null @@ -1,152 +0,0 @@ -const tape = require('tape') -const { Kappa } = require('..') -const hypercore = require('hypercore') -const ram = require('random-access-memory') -const hypercoreSource = require('../sources/hypercore') - -// Has to be updated. -tape.skip('stacked views run in order', t => { - const kappa = new Kappa() - const core = hypercore(ram, { valueEncoding: 'json' }) - kappa.source('core', hypercoreSource, { feed: core }) - - let first = 0 - let second = 0 - let firstOpn, secondOpn - const stack = [ - { - name: 'first', - open (cb) { - t.equal(secondOpn, undefined, 'first opens first') - firstOpn = true - cb() - }, - map (msgs, next) { - kappa.api.second.count((count) => { - t.equal(count, first, 'first ran first') - first = first + msgs.length - next() - }) - }, - api: { - count (kappa, cb) { process.nextTick(cb, first) } - } - }, - { - name: 'second', - open (cb) { - t.equal(firstOpn, true, 'second opens last') - secondOpn = true - cb() - }, - map (msgs, next) { - kappa.api.first.count((count) => { - t.equal(count, second + msgs.length, 'second ran second') - second = second + msgs.length - next() - }) - }, - api: { - count (kappa, cb) { process.nextTick(cb, second) } - } - } - ] - - kappa.useStack('stack', stack) - - core.append([1, 2, 3]) - setTimeout(() => { - core.append([1, 2, 3]) - kappa.ready(() => { - t.equal(first, 6) - t.equal(second, 6) - t.end() - }) - }, 10) -}) - -tape.skip('stacked view linked kv', t => { - const kappa = new Kappa() - const core = hypercore(ram, { valueEncoding: 'json' }) - kappa.source('core', hypercoreSource, { feed: core }) - const core2 = hypercore(ram, { valueEncoding: 'json' }) - kappa.source('core2', hypercoreSource, { feed: core2 }) - - const links = {} - const keys = {} - const ops = [] - kappa.useStack('stack', [ - { - name: 'links', - map (msgs, next) { - msgs.forEach(msg => { - const id = toId(msg) - if (msg.value.links) msg.value.links.forEach(l => (links[l] = true)) - const currentLinks = kappa.api.links.get(msg.value.key) - keys[msg.value.key] = [...currentLinks, id] - }) - next() - }, - api: { - isLinked (kappa, msg) { - return !!links[toId(msg)] - }, - get (kappa, key) { - return keys[key] || [] - } - } - }, - { - name: 'ops', - map (msgs, next) { - msgs.forEach(msg => { - let op - if (kappa.api.links.isLinked(msg)) { - op = 'outdated' - } else if (kappa.api.links.get(msg.value.key).length > 1) { - op = 'update' - } else { - op = 'create' - } - ops.push({ op, value: msg.value, id: toId(msg), idx: ops.length }) - }) - next() - }, - api: { - log (kappa) { - return ops - } - } - } - ]) - - readyAll([core, core2], () => { - core.append({ key: 'earth', value: 'best' }) - core.append({ key: 'mars', value: 'red', links: [core2.key.toString('hex') + '@0'] }) - core.append({ key: 'earth', value: 'burned', links: [core.key.toString('hex') + '@0'] }) - - setTimeout(() => { - core2.append({ key: 'mars', value: 'hotter' }) - core2.append({ key: 'earth', value: 'fuuuu' }) - }, 0) - - setTimeout(() => { - kappa.ready('ops', () => { - let log = kappa.api.ops.log() - t.equal(log.filter(l => l.op === 'create').length, 2, 'two creations') - t.equal(log.filter(l => l.op === 'update').length, 2, 'two updates') - t.equal(log.filter(l => l.op === 'outdated').length, 1, 'one outdated') - t.end() - }) - }, 10) - }) -}) - -function toId (msg) { - return msg.key + '@' + msg.seq -} - -function readyAll (cores, cb) { - let pending = cores.length - cores.forEach(core => core.ready(() => (--pending === 0 && cb()))) -} From d32bc9da1405f8c57b121aeee92efcb177f66e2e Mon Sep 17 00:00:00 2001 From: "Franz Heinzmann (Frando)" Date: Sat, 2 May 2020 17:35:10 +0200 Subject: [PATCH 72/85] corrections --- README.md | 2 +- sources/util/state.js | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 07e8ebe..4c9d4ec 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ When calling `kappa.use()` a new *Flow* is created. A Flow is the combination of Example: ```javascript -const { KappaCore, SimpleState } = require('kappa-core') +const { Kappa, SimpleState } = require('kappa-core') function createSource (opts) { const state = new SimpleState({ db: opts.db }) return { diff --git a/sources/util/state.js b/sources/util/state.js index b141555..ed219d5 100644 --- a/sources/util/state.js +++ b/sources/util/state.js @@ -74,7 +74,6 @@ class FakeDB { err.type = 'NotFoundError' err.notFound = true process.nextTick(cb, err) - cb(err) } else { process.nextTick(cb, null, this.state[key]) } From 462fc1a63dfe3027ef85a5fa78174ba8cb5c9155 Mon Sep 17 00:00:00 2001 From: "Franz Heinzmann (Frando)" Date: Mon, 4 May 2020 20:16:40 +0200 Subject: [PATCH 73/85] Prepare for API changes --- kappa.js | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/kappa.js b/kappa.js index 1ea3791..2094d01 100644 --- a/kappa.js +++ b/kappa.js @@ -259,37 +259,37 @@ class Flow extends EventEmitter { this._source.pull(onbatch) function onbatch (result) { - if (self.status === Status.Paused) return close() if (!result) return close() - if (result.messages) result.messages = result.messages.filter(m => m) - if (!result.messages.length) return close(null, result) - - const { messages = [], finished, onindexed } = result + const err = null + let { messages, finished, onindexed } = result + if (err) return close(err) + if (self.status === Status.Paused) return + if (!messages) return close() + messages = messages.filter(m => m) + if (!messages.length) return close() self._transform.run(messages, messages => { - if (!messages.length) return close(null, { messages, finished, onindexed }) + if (!messages.length) return close() // TODO: Handle timeout? self._view.map(messages, err => { - close(err, { messages, finished, onindexed }) + close(err, messages, finished, onindexed) }) }) } - function close (err, result) { - if (err) { - self.emit('error', err) - return finish(false) - } - if (!result) return finish(true) - const { messages, finished, onindexed } = result - if (messages.length && self._view.indexed) { + function close (err, messages, finished, onindexed) { + if (err) return finish(err) + if (messages && messages.length && self._view.indexed) { self._view.indexed(messages) } - if (onindexed) onindexed(() => finish(finished)) - else finish(finished) + if (onindexed) onindexed(() => finish(null, finished)) + else finish(null, finished) } - function finish (finished) { + function finish (err, finished = true) { + if (err) { + self.emit('error', err) + } self.status = Status.Ready if (self._closing) return self.emit('ready') if (self.incomingUpdate || !finished) { From 5b04ad84c5b4a50284126c755e402b64db9ff333 Mon Sep 17 00:00:00 2001 From: "Franz Heinzmann (Frando)" Date: Wed, 6 May 2020 23:16:29 +0200 Subject: [PATCH 74/85] add error state --- kappa.js | 17 ++++++++---- test/basic.js | 76 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 6 deletions(-) diff --git a/kappa.js b/kappa.js index 2094d01..93f64ac 100644 --- a/kappa.js +++ b/kappa.js @@ -2,9 +2,10 @@ const thunky = require('thunky') const { EventEmitter } = require('events') const Status = { - Ready: 0, - Running: 1, - Paused: 2 + Ready: 'ready', + Running: 'running', + Paused: 'paused', + Error: 'error' } module.exports = class Kappa extends EventEmitter { @@ -260,9 +261,8 @@ class Flow extends EventEmitter { function onbatch (result) { if (!result) return close() - const err = null - let { messages, finished, onindexed } = result - if (err) return close(err) + let { error, messages, finished, onindexed } = result + if (error) return close(error) if (self.status === Status.Paused) return if (!messages) return close() messages = messages.filter(m => m) @@ -288,10 +288,15 @@ class Flow extends EventEmitter { function finish (err, finished = true) { if (err) { + self.status = Status.Error self.emit('error', err) + if (self._closing) self.emit('ready') + return } + self.status = Status.Ready if (self._closing) return self.emit('ready') + if (self.incomingUpdate || !finished) { self.incomingUpdate = false process.nextTick(self._run.bind(self)) diff --git a/test/basic.js b/test/basic.js index 0087d72..d1190ca 100644 --- a/test/basic.js +++ b/test/basic.js @@ -27,6 +27,82 @@ tape('simple source', t => { ]) }) +tape('finished handling', t => { + const kappa = new Kappa() + t.plan(5) + + let msgs = ['a', 'b', 'c'] + let i = 0 + kappa.use('foo', { + pull (next) { + let finished + if (i !== msgs.length - 1) finished = false + next({ + messages: [msgs[i]], + finished, + onindexed: (cb) => { + t.pass('onindexed ' + i) + i = i + 1 + cb() + } + }) + } + }, createSimpleView()) + + runAll([ + cb => kappa.view.foo.collect((err, res) => { + t.error(err) + t.deepEqual(res, ['a', 'b', 'c']) + cb() + }), + cb => t.end() + ]) +}) + +tape('error on pull', t => { + const kappa = new Kappa() + let msgs = ['a'] + let i = 0 + kappa.use('foo', { + pull (next) { + if (i === 1) return next({ error: new Error('pull error') }) + if (i > 1) t.fail('pull after error') + next({ + messages: msgs, + finished: false, + onindexed: (cb) => { + t.pass('onindexed ' + i) + i++ + cb() + } + }) + } + }, createSimpleView()) + kappa.once('error', err => { + t.equal(err.message, 'pull error') + t.equal(kappa.flows.foo.status, 'error') + t.end() + }) +}) + +tape('error on map', t => { + const kappa = new Kappa() + kappa.use('foo', createSimpleSource(), { + map (messages, next) { + next(new Error('map error')) + } + }) + kappa.source.foo.push('a') + kappa.once('error', err => { + t.equal(err.message, 'map error') + t.equal(kappa.flows.foo.status, 'error') + t.end() + }) + kappa.ready(() => { + t.fail('no ready on error') + }) +}) + tape('reset', t => { const kappa = new Kappa() const foo = kappa.use('foo', createSimpleSource(), createSimpleView()) From 33e75320d10b57532e669a64918325bb153a6687 Mon Sep 17 00:00:00 2001 From: "Franz Heinzmann (Frando)" Date: Wed, 6 May 2020 23:23:00 +0200 Subject: [PATCH 75/85] track indexing state * allow sources to return a state object that describes the indexing progress * exposes a getState method * emit a state-update event --- README.md | 24 ++++++++++++++++++++- kappa.js | 30 +++++++++++++++++++------- test/basic.js | 59 +++++++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 102 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 4c9d4ec..d2bc51f 100644 --- a/README.md +++ b/README.md @@ -25,9 +25,19 @@ Register a flow. * `pull: function (next)`: **(required)** Handler to pull new messages from the view. Should call `next` with either nothing or an object that looks like this: ```javascript { + error: Error, messages: [], // array of messages finished: true, // if set to false, signal that more messages are pending - onindexed: function (cb) {} // will be called when the view finished indexing + onindexed: function (cb) { + // will be called when the view finished indexing + // call cb after the source state is updated + // may return a state object with, by convention, the following keys: + cb(null, { + totalBlocks: Number, + indexedBlocks: Number, + prevIndexedBlocks: Number + }) + } } ``` * `reset: function (cb)`: **(required)** Handler to reset internal state. This is called when a full reindex is necessary. This means that the next pull ought to start at the beginning. @@ -71,6 +81,18 @@ When calling `kappa.use()` a new *Flow* is created. A Flow is the combination of * `flow.name`: (string) A name that uniquely identifies this flow within the Kappa core. * `flow.update()`: Signal to the flow that the source has new data available. Youwant to call this from a source when the source has new data. If the Kappa core is not paused, this will cause the `pull` handler to be called. * `flow.ready(cb)`: Calls `cb` (with no arguments) when this flow has finished processing all messages. `cb` is called immediately if the flow is already finished. +* `flow.getState()`: Get the current indexing state. Returns an object: + ```javascript + { + status: 'idle' | 'running' | 'paused' | 'error', + error: null | Error, + // ... other keys as returned by the source + // by convention this should include the following keys: + totalBlocks: Number, + indexedBlocks: Number, + prevIndexedBlocks: Number + } + ``` * `flow.view`: Object with the view's API functions * `flow.source`: Object with the source's API functions diff --git a/kappa.js b/kappa.js index 93f64ac..fd69f7c 100644 --- a/kappa.js +++ b/kappa.js @@ -110,6 +110,7 @@ class Flow extends EventEmitter { this.context = opts.context this.status = opts.status || Status.Ready + this._indexingState = {} // Assign view and source apis this.view = {} @@ -249,6 +250,10 @@ class Flow extends EventEmitter { process.nextTick(this._run.bind(this)) } + getState () { + return { status: this.status, ...this._indexingState } + } + _run () { const self = this if (!this.opened) return @@ -257,14 +262,18 @@ class Flow extends EventEmitter { this.status = Status.Running + this.emit('state-update', self.getState()) + this._source.pull(onbatch) function onbatch (result) { + if (self.status === Status.Paused) return + if (!result) return close() let { error, messages, finished, onindexed } = result if (error) return close(error) - if (self.status === Status.Paused) return if (!messages) return close() + messages = messages.filter(m => m) if (!messages.length) return close() @@ -282,26 +291,31 @@ class Flow extends EventEmitter { if (messages && messages.length && self._view.indexed) { self._view.indexed(messages) } - if (onindexed) onindexed(() => finish(null, finished)) - else finish(null, finished) + if (onindexed) { + onindexed((err, status) => { + if (!err && status) self._indexingState = Object.assign(self._indexingState, { error: null }, status) + finish(err, finished) + }) + } else finish(null, finished) } function finish (err, finished = true) { if (err) { self.status = Status.Error + self._indexingState.error = err self.emit('error', err) - if (self._closing) self.emit('ready') - return + } else { + self.status = Status.Ready } - self.status = Status.Ready if (self._closing) return self.emit('ready') - if (self.incomingUpdate || !finished) { + if (!err && (self.incomingUpdate || !finished)) { self.incomingUpdate = false process.nextTick(self._run.bind(self)) } else { - self.emit('ready') + self.emit('state-update', self.getState()) + if (!err) self.emit('ready') } } } diff --git a/test/basic.js b/test/basic.js index d1190ca..e443c53 100644 --- a/test/basic.js +++ b/test/basic.js @@ -103,6 +103,49 @@ tape('error on map', t => { }) }) +tape('state update', t => { + const kappa = new Kappa() + const foo = kappa.use('foo', createSimpleSource(), createSimpleView()) + let state + foo.on('state-update', newState => { + state = newState + }) + foo.source.push([1, 2]) + process.nextTick(() => { + foo.source.push([3, 4]) + }) + runAll([ + cb => setTimeout(cb, 0), + cb => foo.view.collect((err, res) => { + t.error(err, 'no error') + t.deepEqual(res, [1, 2, 3, 4], 'result matches') + t.deepEqual(state, { + status: 'ready', + error: null, + totalBlocks: 4, + indexedBlocks: 4, + prevIndexedBlocks: 2 + }, 'state matches') + cb() + }), + cb => { + kappa.once('error', err => { + t.equal(err.message, 'bad') + process.nextTick(cb) + }) + foo.source.error(new Error('bad')) + }, + cb => { + t.equal(state.status, 'error') + t.equal(state.error.message, 'bad') + t.equal(foo.getState().status, 'error') + t.equal(foo.getState().error.message, 'bad') + cb() + }, + cb => t.end() + ]) +}) + tape('reset', t => { const kappa = new Kappa() const foo = kappa.use('foo', createSimpleSource(), createSimpleView()) @@ -232,6 +275,7 @@ function createSimpleSource (opts = {}) { const maxBatch = opts.maxBatch || 10 let flow = null let state = 0 + let error = null const source = { open (_flow, next) { @@ -239,15 +283,21 @@ function createSimpleSource (opts = {}) { next() }, pull (next) { + if (error) return next({ error }) const max = buf.length const end = Math.min(state + maxBatch, max) const messages = buf.slice(state, end) + const lastState = state next({ messages, finished: end === max, onindexed (cb) { state = end - cb() + cb(null, { + totalBlocks: buf.length, + indexedBlocks: end, + prevIndexedBlocks: lastState + }) } }) }, @@ -258,7 +308,12 @@ function createSimpleSource (opts = {}) { get api () { return { push (kappa, value) { - buf.push(value) + if (!Array.isArray(value)) value = [value] + buf.push(...value) + if (flow) flow.update() + }, + error (kappa, err) { + error = err if (flow) flow.update() } } From 567b0bbec05cbc3a13b6462c5581e8171fd8bf9b Mon Sep 17 00:00:00 2001 From: "Franz Heinzmann (Frando)" Date: Wed, 6 May 2020 23:31:43 +0200 Subject: [PATCH 76/85] mark status property private --- kappa.js | 36 ++++++++++++++++++------------------ test/basic.js | 5 +++-- 2 files changed, 21 insertions(+), 20 deletions(-) diff --git a/kappa.js b/kappa.js index fd69f7c..1685910 100644 --- a/kappa.js +++ b/kappa.js @@ -108,8 +108,8 @@ class Flow extends EventEmitter { this._view = view this._source = source - this.context = opts.context - this.status = opts.status || Status.Ready + this._context = opts.context + this._status = opts.status || Status.Ready this._indexingState = {} // Assign view and source apis @@ -118,12 +118,12 @@ class Flow extends EventEmitter { this.view.ready = cb => this.ready(cb) if (view.api) { for (let [key, value] of Object.entries(view.api)) { - this.view[key] = bindFn(value, this, this.context) + this.view[key] = bindFn(value, this, this._context) } } if (source.api) { for (let [key, value] of Object.entries(source.api)) { - this.source[key] = bindFn(value, this, this.context) + this.source[key] = bindFn(value, this, this._context) } } @@ -183,7 +183,7 @@ class Flow extends EventEmitter { const self = this this.pause() this._closing = true - if (this.status === Status.Running) return this.once('ready', close) + if (this._status === Status.Running) return this.once('ready', close) else close() function close () { let pending = 1 @@ -211,19 +211,19 @@ class Flow extends EventEmitter { function onsourceready () { process.nextTick(() => { - if (self.status === Status.Ready) process.nextTick(cb) + if (self._status === Status.Ready) process.nextTick(cb) else self.once('ready', cb) }) } } pause () { - this.status = Status.Paused + this._status = Status.Paused } resume () { - if (this.status !== Status.Paused) return - this.status = Status.Ready + if (this._status !== Status.Paused) return + this._status = Status.Ready if (!this.opened) return this.open() this._run() } @@ -251,23 +251,23 @@ class Flow extends EventEmitter { } getState () { - return { status: this.status, ...this._indexingState } + return { status: this._status, ...this._indexingState } } _run () { const self = this if (!this.opened) return - if (this.status === Status.Running) return - if (this.status === Status.Paused) return + if (this._status === Status.Running) return + if (this._status === Status.Paused) return - this.status = Status.Running + this._status = Status.Running this.emit('state-update', self.getState()) this._source.pull(onbatch) function onbatch (result) { - if (self.status === Status.Paused) return + if (self._status === Status.Paused) return if (!result) return close() let { error, messages, finished, onindexed } = result @@ -292,8 +292,8 @@ class Flow extends EventEmitter { self._view.indexed(messages) } if (onindexed) { - onindexed((err, status) => { - if (!err && status) self._indexingState = Object.assign(self._indexingState, { error: null }, status) + onindexed((err, _status) => { + if (!err && _status) self._indexingState = Object.assign(self._indexingState, { error: null }, _status) finish(err, finished) }) } else finish(null, finished) @@ -301,11 +301,11 @@ class Flow extends EventEmitter { function finish (err, finished = true) { if (err) { - self.status = Status.Error + self._status = Status.Error self._indexingState.error = err self.emit('error', err) } else { - self.status = Status.Ready + self._status = Status.Ready } if (self._closing) return self.emit('ready') diff --git a/test/basic.js b/test/basic.js index e443c53..8dc3839 100644 --- a/test/basic.js +++ b/test/basic.js @@ -80,7 +80,7 @@ tape('error on pull', t => { }, createSimpleView()) kappa.once('error', err => { t.equal(err.message, 'pull error') - t.equal(kappa.flows.foo.status, 'error') + t.equal(kappa.flows.foo.getState().status, 'error') t.end() }) }) @@ -95,7 +95,7 @@ tape('error on map', t => { kappa.source.foo.push('a') kappa.once('error', err => { t.equal(err.message, 'map error') - t.equal(kappa.flows.foo.status, 'error') + t.equal(kappa.flows.foo.getState().status, 'error') t.end() }) kappa.ready(() => { @@ -126,6 +126,7 @@ tape('state update', t => { indexedBlocks: 4, prevIndexedBlocks: 2 }, 'state matches') + t.deepEqual(state, foo.getState()) cb() }), cb => { From 0f3bc6986c311bd11f4748c875e9779bc134063f Mon Sep 17 00:00:00 2001 From: "Franz Heinzmann (Frando)" Date: Thu, 7 May 2020 02:03:26 +0200 Subject: [PATCH 77/85] move test util libs --- test/basic.js | 80 +-------------------------------------- test/lib/simple-source.js | 52 +++++++++++++++++++++++++ test/lib/simple-view.js | 27 +++++++++++++ 3 files changed, 81 insertions(+), 78 deletions(-) create mode 100644 test/lib/simple-source.js create mode 100644 test/lib/simple-view.js diff --git a/test/basic.js b/test/basic.js index 8dc3839..2a682b3 100644 --- a/test/basic.js +++ b/test/basic.js @@ -1,6 +1,8 @@ const tape = require('tape') const { Kappa } = require('..') const { runAll } = require('./lib/util') +const createSimpleView = require('./lib/simple-view') +const createSimpleSource = require('./lib/simple-source') tape('simple source', t => { const kappa = new Kappa() @@ -245,81 +247,3 @@ tape('fetch version error', t => { t.end() }) }) - -function createSimpleView () { - let res = [] - let clears = 0 - const view = { - map (msgs, next) { - res = res.concat(msgs) - next() - }, - reset (cb) { - clears = clears + 1 - res = [] - cb() - }, - api: { - collect (kappa, cb) { - this.ready(() => cb(null, res)) - }, - clearedCount (kappa) { - return clears - } - } - } - return view -} - -function createSimpleSource (opts = {}) { - const buf = [] - const maxBatch = opts.maxBatch || 10 - let flow = null - let state = 0 - let error = null - - const source = { - open (_flow, next) { - flow = _flow - next() - }, - pull (next) { - if (error) return next({ error }) - const max = buf.length - const end = Math.min(state + maxBatch, max) - const messages = buf.slice(state, end) - const lastState = state - next({ - messages, - finished: end === max, - onindexed (cb) { - state = end - cb(null, { - totalBlocks: buf.length, - indexedBlocks: end, - prevIndexedBlocks: lastState - }) - } - }) - }, - reset (next) { - state = 0 - next() - }, - get api () { - return { - push (kappa, value) { - if (!Array.isArray(value)) value = [value] - buf.push(...value) - if (flow) flow.update() - }, - error (kappa, err) { - error = err - if (flow) flow.update() - } - } - } - } - - return source -} diff --git a/test/lib/simple-source.js b/test/lib/simple-source.js new file mode 100644 index 0000000..914b1e9 --- /dev/null +++ b/test/lib/simple-source.js @@ -0,0 +1,52 @@ +module.exports = function createSimpleSource (opts = {}) { + const buf = [] + const maxBatch = opts.maxBatch || 10 + let flow = null + let state = 0 + let error = null + + const source = { + open (_flow, next) { + flow = _flow + next() + }, + pull (next) { + if (error) return next({ error }) + const max = buf.length + const end = Math.min(state + maxBatch, max) + const messages = buf.slice(state, end) + const lastState = state + next({ + messages, + finished: end === max, + onindexed (cb) { + state = end + cb(null, { + totalBlocks: buf.length, + indexedBlocks: end, + prevIndexedBlocks: lastState + }) + } + }) + }, + reset (next) { + state = 0 + next() + }, + get api () { + return { + push (kappa, value) { + if (!Array.isArray(value)) value = [value] + buf.push(...value) + if (flow) flow.update() + }, + error (kappa, err) { + error = err + if (flow) flow.update() + } + } + } + } + + return source +} diff --git a/test/lib/simple-view.js b/test/lib/simple-view.js new file mode 100644 index 0000000..955f2f7 --- /dev/null +++ b/test/lib/simple-view.js @@ -0,0 +1,27 @@ +module.exports = function createSimpleView () { + let res = [] + let clears = 0 + const view = { + map (msgs, next) { + res = res.concat(msgs) + process.nextTick(next) + }, + reset (cb) { + clears = clears + 1 + res = [] + cb() + }, + api: { + collect (kappa, cb) { + this.ready(() => cb(null, res)) + }, + count (kappa) { + return res.eength + }, + clearedCount (kappa) { + return clears + } + } + } + return view +} From 111d683cd4c3e5f7077c7a7800ce0ccf2fc66876 Mon Sep 17 00:00:00 2001 From: "Franz Heinzmann (Frando)" Date: Thu, 7 May 2020 02:06:36 +0200 Subject: [PATCH 78/85] cleanup --- kappa.js | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/kappa.js b/kappa.js index 1685910..79f0951 100644 --- a/kappa.js +++ b/kappa.js @@ -113,19 +113,9 @@ class Flow extends EventEmitter { this._indexingState = {} // Assign view and source apis - this.view = {} - this.source = {} + this.view = bindApi(view.api, this._context) this.view.ready = cb => this.ready(cb) - if (view.api) { - for (let [key, value] of Object.entries(view.api)) { - this.view[key] = bindFn(value, this, this._context) - } - } - if (source.api) { - for (let [key, value] of Object.entries(source.api)) { - this.source[key] = bindFn(value, this, this._context) - } - } + this.source = bindApi(source.api, this._context) // Create the list of funtions through which messages run between pull and map. this._transform = new Pipeline() @@ -230,6 +220,7 @@ class Flow extends EventEmitter { reset (cb = noop) { const self = this + const paused = this._status === Status.Paused this.pause() let pending = 1 process.nextTick(() => { @@ -239,7 +230,7 @@ class Flow extends EventEmitter { }) function done () { if (--pending !== 0) return - self.resume() + if (!paused) self.resume() cb() } } @@ -323,9 +314,13 @@ class Flow extends EventEmitter { // Utils -function bindFn (value, ...binds) { - if (typeof value === 'function') value = value.bind(...binds) - return value +function bindApi (api, ...binds) { + if (!api) return {} + for (let [key, value] of Object.entries(api)) { + if (typeof value !== 'function') continue + api[key] = value.bind(api, ...binds) + } + return api } class Pipeline { From c57c658113a4f34d6efbafad09be55e1ed403ead Mon Sep 17 00:00:00 2001 From: "Franz Heinzmann (Frando)" Date: Thu, 7 May 2020 02:06:47 +0200 Subject: [PATCH 79/85] remove unneeded deps --- package.json | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/package.json b/package.json index 0d28536..55d0d73 100644 --- a/package.json +++ b/package.json @@ -15,26 +15,18 @@ }, "keywords": [], "dependencies": { - "inherits": "^2.0.4", - "stream-collector": "^1.0.1", "thunky": "^1.1.0" }, "devDependencies": { - "bitfield-db": "^1.2.2", "corestore": "^5.0.0", "hypercore": "^8", - "hypercore-crypto": "^1.0.0", "hypercore-protocol": "^7.10.0", - "hyperdrive": "^10.8.18", - "hyperdrive-schemas": "beta", "level-mem": "^5.0.1", "multifeed": "^5", "nanoiterator": "^1.2.0", - "nanoresource": "^1.3.0", - "random-access-file": "^2.1.3", "random-access-memory": "^3.1.1", "standard": "~12.0.1", - "tape": "^4.11.0" + "tape": "^5.0" }, "license": "ISC" } From b29f5f221d2a097b97c70e3e2eaca84712060e11 Mon Sep 17 00:00:00 2001 From: "Franz Heinzmann (Frando)" Date: Thu, 7 May 2020 02:07:07 +0200 Subject: [PATCH 80/85] fix corestore source --- sources/corestore.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/sources/corestore.js b/sources/corestore.js index 4dbd8ea..5f50c0c 100644 --- a/sources/corestore.js +++ b/sources/corestore.js @@ -5,6 +5,7 @@ const SimpleState = require('./util/state') module.exports = function corestoreSource (opts) { const state = opts.state || new SimpleState(opts) const store = opts.store + const feeds = new Set() const sources = [] return { open (flow, cb) { @@ -29,7 +30,9 @@ module.exports = function corestoreSource (opts) { } } - function _onfeed (flow, feed, cb) { + function _onfeed (flow, feed) { + if (feeds.has(feed)) return + feeds.add(feed) const source = hypercoreSource({ feed, state @@ -37,7 +40,6 @@ module.exports = function corestoreSource (opts) { sources.push(source) source.open(flow, () => { flow.update() - if (cb) cb() }) } } From da208c947bfca6707353a27cd26f72722669bcc7 Mon Sep 17 00:00:00 2001 From: "Franz Heinzmann (Frando)" Date: Fri, 29 May 2020 12:31:27 +0200 Subject: [PATCH 81/85] Emit state-update event on kappa --- kappa.js | 1 + 1 file changed, 1 insertion(+) diff --git a/kappa.js b/kappa.js index 79f0951..e026c5a 100644 --- a/kappa.js +++ b/kappa.js @@ -32,6 +32,7 @@ module.exports = class Kappa extends EventEmitter { this.view[name] = flow.view this.source[name] = flow.source flow.on('error', err => this.emit('error', err, flow)) + flow.on('state-update', state => this.emit('state-update', name, state)) if (this.status !== Status.Paused) { process.nextTick(() => flow.open(err => { From afa09ba897bf953ab6a8f0599c553023027474b1 Mon Sep 17 00:00:00 2001 From: "Franz Heinzmann (Frando)" Date: Fri, 29 May 2020 12:32:55 +0200 Subject: [PATCH 82/85] Reuse iteration code on kappa --- kappa.js | 54 ++++++++++++++++++++++++++++++++---------------------- 1 file changed, 32 insertions(+), 22 deletions(-) diff --git a/kappa.js b/kappa.js index e026c5a..463b4c2 100644 --- a/kappa.js +++ b/kappa.js @@ -16,6 +16,7 @@ module.exports = class Kappa extends EventEmitter { constructor (opts = {}) { super() this.flows = {} + this.status = Status.Ready // APIs this.view = {} this.source = {} @@ -46,45 +47,54 @@ module.exports = class Kappa extends EventEmitter { pause () { this.status = Status.Paused - Object.values(this.flows).forEach(flow => flow.pause()) + this._forEach(flow => flow.pause()) } resume () { if (this.status !== Status.Paused) return - Object.values(this.flows).forEach(flow => flow.resume()) + this._forEach(flow => flow.resume()) this.status = Status.Ready } - reset (name, cb) { - const flow = this.flows[name] - if (!flow) return cb(new Error('Unknown flow: ' + name)) - flow.reset(cb) + reset (names, cb) { + this._forEachAsync((flow, next) => { + flow.reset(next) + }, names, cb) } ready (names, cb) { - if (typeof names === 'function') return this.ready(null, names) + this._forEachAsync((flow, next) => { + flow.ready(next) + }, names, cb) + } + + close (cb) { + this._forEachAsync((flow, next) => { + flow.close(next) + }, cb) + } + + _forEach (fn, names) { if (typeof names === 'string') names = [names] if (!names) names = Object.keys(this.flows) - if (!names.length) return cb() - cb = once(cb) - - let pending = names.length for (const name of names) { - const flow = this.flows[name] - if (!flow) return cb(new Error('Unknown flow: ' + name)) - flow.ready(done) - } - function done () { - if (--pending === 0) cb() + if (!this.flows[name]) continue + fn(this.flows[name]) } } - close (cb) { + _forEachAsync (fn, names, cb) { + if (typeof names === 'function') { + cb = names + names = null + } cb = once(cb) - let flows = Object.values(this.flows) - let pending = flows.length - if (!pending) return done() - flows.forEach(flow => flow.close(done)) + let pending = 1 + this._forEach(flow => { + ++pending + fn(flow, done) + }, names) + done() function done (err) { if (err) return cb(err) if (--pending === 0) cb() From 381edb30bb2a4a29751db34c7fc330e9c9b2c10e Mon Sep 17 00:00:00 2001 From: "Franz Heinzmann (Frando)" Date: Fri, 29 May 2020 12:36:28 +0200 Subject: [PATCH 83/85] Better state tracking --- kappa.js | 93 +++++++++++++++++++++++++++++++-------------------- test/basic.js | 1 - 2 files changed, 57 insertions(+), 37 deletions(-) diff --git a/kappa.js b/kappa.js index 463b4c2..9665bf2 100644 --- a/kappa.js +++ b/kappa.js @@ -2,9 +2,11 @@ const thunky = require('thunky') const { EventEmitter } = require('events') const Status = { + Closed: 'closed', Ready: 'ready', Running: 'running', Paused: 'paused', + Closing: 'closing', Error: 'error' } @@ -120,7 +122,6 @@ class Flow extends EventEmitter { this._source = source this._context = opts.context - this._status = opts.status || Status.Ready this._indexingState = {} // Assign view and source apis @@ -136,6 +137,7 @@ class Flow extends EventEmitter { this.opened = false this.open = thunky(this._open.bind(this)) + this._state = new State() } get version () { @@ -174,6 +176,7 @@ class Flow extends EventEmitter { if (done) return done = true if (err) return cb(err) + self._setState(Status.Ready) self.opened = true self._run() cb() @@ -183,9 +186,12 @@ class Flow extends EventEmitter { close (cb) { const self = this this.pause() - this._closing = true - if (this._status === Status.Running) return this.once('ready', close) + let state = this._state.state + this._setState(Status.Closing) + + if (state === Status.Running) return this.once('ready', close) else close() + function close () { let pending = 1 if (self._source.close) ++pending && self._source.close(done) @@ -193,7 +199,7 @@ class Flow extends EventEmitter { done() function done () { if (--pending !== 0) return - self._closing = false + self._setState(Status.Closed) self.opened = false cb() } @@ -212,26 +218,26 @@ class Flow extends EventEmitter { function onsourceready () { process.nextTick(() => { - if (self._status === Status.Ready) process.nextTick(cb) + if (self._state.state === Status.Ready) process.nextTick(cb) else self.once('ready', cb) }) } } pause () { - this._status = Status.Paused + this._setState(Status.Paused) } resume () { - if (this._status !== Status.Paused) return - this._status = Status.Ready + if (this._state.state !== Status.Paused) return if (!this.opened) return this.open() + this._setState(Status.Ready) this._run() } reset (cb = noop) { const self = this - const paused = this._status === Status.Paused + const paused = this._state.state === Status.Paused this.pause() let pending = 1 process.nextTick(() => { @@ -253,29 +259,33 @@ class Flow extends EventEmitter { } getState () { - return { status: this._status, ...this._indexingState } + return this._state.get() + } + + _setState (state, context) { + this._state.set(state, context) + this.emit('state-update', this._state.get()) + if (state === Status.Error) { + this.emit('error', context && context.error) + } } _run () { + if (this._state.state !== Status.Ready) return const self = this - if (!this.opened) return - if (this._status === Status.Running) return - if (this._status === Status.Paused) return - - this._status = Status.Running - - this.emit('state-update', self.getState()) + this._setState(Status.Running) this._source.pull(onbatch) function onbatch (result) { - if (self._status === Status.Paused) return - + // If set to paused while pulling, drop the result and don't update state. + if (self._state.state === Status.Paused) return if (!result) return close() + let { error, messages, finished, onindexed } = result + if (error) return close(error) if (!messages) return close() - messages = messages.filter(m => m) if (!messages.length) return close() @@ -294,35 +304,46 @@ class Flow extends EventEmitter { self._view.indexed(messages) } if (onindexed) { - onindexed((err, _status) => { - if (!err && _status) self._indexingState = Object.assign(self._indexingState, { error: null }, _status) - finish(err, finished) - }) - } else finish(null, finished) + onindexed((err, context) => finish(err, finished, context)) + } else { + finish(null, finished) + } } - function finish (err, finished = true) { + function finish (err, finished = true, context) { if (err) { - self._status = Status.Error - self._indexingState.error = err - self.emit('error', err) - } else { - self._status = Status.Ready + self._setState(Status.Error, { error: err }) + } else if (self._state.state !== Status.Closing) { + self._setState(Status.Ready, context) } - if (self._closing) return self.emit('ready') - - if (!err && (self.incomingUpdate || !finished)) { + if (self._state.state === Status.Ready && (self.incomingUpdate || !finished)) { self.incomingUpdate = false process.nextTick(self._run.bind(self)) } else { - self.emit('state-update', self.getState()) - if (!err) self.emit('ready') + self.emit('ready') } + } } } +class State { + constructor () { + this.state = Status.Closed + this.context = null + } + + set (state, context) { + this.state = state + if (context) this.context = { ...this.context, ...context } + } + + get () { + return Object.assign({ status: this.state }, this.context || {}) + } +} + // Utils function bindApi (api, ...binds) { diff --git a/test/basic.js b/test/basic.js index 2a682b3..7547a8d 100644 --- a/test/basic.js +++ b/test/basic.js @@ -123,7 +123,6 @@ tape('state update', t => { t.deepEqual(res, [1, 2, 3, 4], 'result matches') t.deepEqual(state, { status: 'ready', - error: null, totalBlocks: 4, indexedBlocks: 4, prevIndexedBlocks: 2 From def20f1d1f51a3e2c6524818e0cba6fd26ecee12 Mon Sep 17 00:00:00 2001 From: "Franz Heinzmann (Frando)" Date: Tue, 2 Jun 2020 12:36:48 +0200 Subject: [PATCH 84/85] Expose top-level getState --- kappa.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/kappa.js b/kappa.js index 9665bf2..d9352a0 100644 --- a/kappa.js +++ b/kappa.js @@ -64,6 +64,12 @@ module.exports = class Kappa extends EventEmitter { }, names, cb) } + getState () { + const state = {} + this._forEach(flow => (state[flow.name] = flow.getState())) + return state + } + ready (names, cb) { this._forEachAsync((flow, next) => { flow.ready(next) From 187c9f9a4ed98ab1c127c0b41709be96038ae8ce Mon Sep 17 00:00:00 2001 From: "Franz Heinzmann (Frando)" Date: Tue, 2 Jun 2020 12:37:24 +0200 Subject: [PATCH 85/85] Restructure test SimpleSource --- test/lib/simple-source.js | 98 ++++++++++++++++++++------------------- 1 file changed, 51 insertions(+), 47 deletions(-) diff --git a/test/lib/simple-source.js b/test/lib/simple-source.js index 914b1e9..66c0b23 100644 --- a/test/lib/simple-source.js +++ b/test/lib/simple-source.js @@ -1,52 +1,56 @@ -module.exports = function createSimpleSource (opts = {}) { - const buf = [] - const maxBatch = opts.maxBatch || 10 - let flow = null - let state = 0 - let error = null +module.exports = (opts) => new SimpleSource(opts) - const source = { - open (_flow, next) { - flow = _flow - next() - }, - pull (next) { - if (error) return next({ error }) - const max = buf.length - const end = Math.min(state + maxBatch, max) - const messages = buf.slice(state, end) - const lastState = state - next({ - messages, - finished: end === max, - onindexed (cb) { - state = end - cb(null, { - totalBlocks: buf.length, - indexedBlocks: end, - prevIndexedBlocks: lastState - }) - } - }) - }, - reset (next) { - state = 0 - next() - }, - get api () { - return { - push (kappa, value) { - if (!Array.isArray(value)) value = [value] - buf.push(...value) - if (flow) flow.update() - }, - error (kappa, err) { - error = err - if (flow) flow.update() - } +class SimpleSource { + constructor (opts = {}) { + this.buf = opts.data || [] + this.cursor = 0 + this.flow = null + this.error = null + this.maxBatch = opts.maxBatch || 10 + } + + open (flow, next) { + this.flow = flow + next() + } + + pull (next) { + if (this.error) return next({ error: this.error }) + const len = this.buf.length + const end = Math.min(this.cursor + this.maxBatch, len) + const messages = this.buf.slice(this.cursor, end) + const lastState = this.cursor + next({ + messages, + finished: end === len, + onindexed: cb => { + this.cursor = end + cb(null, { + totalBlocks: this.buf.length, + indexedBlocks: end, + prevIndexedBlocks: lastState + }) } - } + }) } - return source + reset (cb) { + this.cursor = 0 + cb() + } + + get api () { + const self = this + return { + push (kappa, value) { + if (!Array.isArray(value)) value = [value] + self.buf.push(...value) + if (self.flow) self.flow.update() + }, + error (kappa, err) { + self.error = err + if (self.flow) self.flow.update() + } + } + } }