UNPKG

@web4/mountable-bittrie

Version:

A Bittrie wrapper that supports mounting of other Bittries

767 lines (649 loc) 23 kB
const p = require('path').posix const { EventEmitter } = require('events') const bittrie = require('@web4/bittrie') const BitProtocol = require('@web4/bit-protocol') const bitwebCrypto = require('@web4/bitweb-crypto') const thunky = require('thunky') const nanoiterator = require('nanoiterator') const toStream = require('nanoiterator/to-stream') const isOptions = require('is-options') const unixify = require('unixify') const Nanoresource = require('nanoresource/emitter') const { Mount } = require('./lib/messages') const Flags = { MOUNT: 1 } const MOUNT_PREFIX = '/mounts' const OWNER = Symbol('mountable-bittrie-owner') class MountableBittrie extends Nanoresource { constructor (chainstore, key, opts = {}) { super() if (key && (typeof key === 'string')) key = Buffer.from(key, 'hex') this.chainstore = chainstore this.key = key this.discoveryKey = this.key ? bitwebCrypto.discoveryKey(this.key) : null this.opts = opts this.sparse = opts.sparse !== false this.subtype = opts.subtype || 'mountable-bittrie' if (opts.valueEncoding) throw new Error('MountableBittrie does not currently support a valueEncoding option.') var feed = this.opts.feed if (!feed) feed = this.chainstore.default({ key, ...this.opts }) if (feed[OWNER]) { this.trie = feed[OWNER] } else { this.trie = opts.trie || bittrie(null, { ...opts, feed, version: null, alwaysUpdate: true, alwaysReconnect: true, subtype: this.subtype }) this.trie.feed[OWNER] = this.trie } if (opts.version) { this.trie = this.trie.checkout(opts.version) } this._unlisteners = [] this.feed = this.trie.feed if (this.trie !== opts.trie) { const errorListener = err => this.emit('error', err) this.trie.on('error', errorListener) this._unlisteners.push(() => this.trie.removeListener('error', errorListener)) } // TODO: Replace with a LRU cache. this._tries = new Map() this._checkouts = new Map() this.once('close', () => { for (const unlisten of this._unlisteners) { unlisten() } this._unlisteners = [] }) } ready (cb) { return this.open(cb) } _open (cb) { this.chainstore.ready(err => { if (err) return cb(err) this.trie.ready(err => { if (err) return cb(err) this.key = this.trie.key this.discoveryKey = this.trie.discoveryKey if (this.feed.writable) this.trie.alwaysUpdate = false this.emit('feed', this.feed, { version: this.opts && this.opts.version }) this.emit('bittrie', this.trie) return cb(null) }) }) } _close (cb) { this.chainstore.close(err => { this.emit('close') return cb(err) }) } _createBittrie (key, opts, cb) { const self = this const keyString = key.toString('hex') var versionedTrie = (opts && opts.version) ? this._checkouts.get(`${keyString}:${opts.version}`) : null if (versionedTrie) return process.nextTick(cb, null, versionedTrie) try { var subfeed = this.chainstore.get({ ...opts, key, version: null }) } catch (err) { err.badKey = true return cb(err) } var trie = this._tries.get(keyString) if (opts && opts.cached) return cb(null, trie) var creating = !trie trie = trie || new MountableBittrie(this.chainstore, key, { ...this.opts, feed: subfeed, sparse: this.sparse }) self._tries.set(keyString, trie) if (creating) { const onfeed = (feed, opts) => this.emit('feed', feed, opts) const ontrie = trie => this.emit('bittrie', trie) self._unlisteners.push(() => trie.removeListener('feed', onfeed)) self._unlisteners.push(() => trie.removeListener('bittrie', ontrie)) trie.on('feed', onfeed) trie.on('bittrie', ontrie) } if (!trie.opened) { trie.ready(err => { if (err) return cb(err) return onready() }) } else process.nextTick(onready) function onready () { if (!opts || !opts.version) return ontrie(trie) versionedTrie = trie.checkout(opts.version) self._checkouts.set(`${keyString}:${opts.version}`, versionedTrie) return ontrie(versionedTrie) } function ontrie (trie) { trie.trie.ready(err => { if (err) return cb(err) return cb(null, trie) }) } } _trieForMountNode (mountNode, opts, cb) { if (typeof opts === 'function') return this._trieForMountNode(mountNode, {}, opts) opts = opts || {} if (!mountNode) return cb(new Error(`Mount metadata not found`)) try { var mountInfo = Mount.decode(mountNode.value) } catch (err) { return cb(err) } this._createBittrie(mountInfo.key, { ...opts, version: mountInfo.version }, (err, trie) => { if (err) return cb(err) return cb(null, trie, mountInfo) }) } _isNormalNode (node) { if (!node) return true return node.flags ^ Flags.MOUNT } _mountInfo () { return { key: this.key, version: this.opts.version ? this.opts.version : null, localPath: '', remotePath: '' } } _maybeSetSymbols (node, trie, mountInfo, innerPath) { if (trie && !node[MountableBittrie.Symbols.TRIE]) node[MountableBittrie.Symbols.TRIE] = trie if (mountInfo && !node[MountableBittrie.Symbols.MOUNT]) node[MountableBittrie.Symbols.MOUNT] = mountInfo if (mountInfo && !node[MountableBittrie.Symbols.INNER_PATH]) node[MountableBittrie.Symbols.INNER_PATH] = innerPath } _getSymbols (node) { return { trie: node[MountableBittrie.Symbols.TRIE], mount: node[MountableBittrie.Symbols.MOUNT], innerPath: node[MountableBittrie.Symbols.INNER_PATH] } } _getSubtrie (path, cb) { this.trie.get(p.join(MOUNT_PREFIX, path), { hidden: true, closest: true }, (err, mountNode) => { if (err) return cb(err) const mountPath = mountNode && mountNode.key.slice(7) if (this._isNormalNode(mountNode) || p.relative(mountPath, path).startsWith('..')) { return cb(null, this.trie, this._mountInfo()) } return this._trieForMountNode(mountNode, cb) }) } get version () { return this.trie.version } static getMetadata (feed, cb) { return bittrie.getMetadata(feed, cb) } getMetadata (cb) { return this.trie.getMetadata(cb) } setMetadata (metadata, cb) { return this.trie.setMetadata(metadata, cb) } getFeed () { if (!this.trie) return null return this.trie.feed } mount (path, key, opts, cb) { if (typeof opts === 'function') return this.mount(path, key, null, opts) path = normalize(path) if (key.length !== 32 && (!opts || !opts.skipValidation)) { const err = new Error('The mount key is not valid.') err.badKey = true return cb(err) } const mountRecord = Mount.encode({ key, localPath: path, remotePath: opts && opts.remotePath && normalize(opts.remotePath), version: opts && opts.version }) this._getSubtrie(path, (err, trie, mountInfo) => { if (err) return cb(err) const innerPath = pathToMount(path, mountInfo) if (!mountInfo.localPath) { return trie.batch([ { type: 'put', key: p.join(MOUNT_PREFIX, innerPath), flags: Flags.MOUNT, hidden: true, value: mountRecord }, // TODO: empty values going to cause harm here? { type: 'put', key: innerPath, flags: Flags.MOUNT, value: (opts && opts.value) || Buffer.alloc(0) } ], err => { if (err) return cb(err) return this._getSubtrie(path, cb) }) } return trie.mount(innerPath, key, opts, err => { if (err) return cb(err) return this._getSubtrie(innerPath, cb) }) }) } unmount (path, cb) { path = normalize(path) return this._getSubtrie(p.dirname(path), (err, trie, mountInfo) => { if (err) return cb(err) const innerPath = pathToMount(path, mountInfo) trie.get(innerPath, (err, node) => { // If the subtrie is a MountableBittrie, use the internal bittrie for the batch. if (trie.trie) trie = trie.trie return trie.batch([ { type: 'del', key: p.join(MOUNT_PREFIX, innerPath), hidden: true }, { type: 'del', key: innerPath } ], cb) }) }) } loadMount (path, cb) { return this._getSubtrie(path, cb) } get (path, opts, cb) { if (typeof opts === 'function') return this.get(path, null, opts) path = normalize(path) const self = this this.trie.get(path, { ...opts, closest: true }, (err, node) => { if (err) return cb(err) const mountInfo = this._mountInfo() if (!node) return cb(null, null, this, mountInfo, path) if (this._isNormalNode(node)) { this._maybeSetSymbols(node, this, mountInfo, path) if (node.key !== path && !(opts && opts.closest)) return cb(null, null, this, mountInfo, path) return cb(null, node, this, mountInfo, path) } if (node.key === path) return cb(null, node, this, mountInfo, path) return this._getSubtrie(path, getFromMount) }) function getFromMount (err, trie, mountInfo) { if (err) return cb(err) const mountPath = pathToMount(path, mountInfo) return trie.get(mountPath, opts, (err, node, subTrie, subMountInfo, subMountPath) => { if (err) return cb(err) subTrie = subTrie || self subMountInfo = subMountInfo || mountInfo subMountPath = subMountPath || mountPath if (!node) return cb(null, null, subTrie, subMountInfo, subMountPath) node.key = pathFromMount(node.key, mountInfo) if (node.key !== path) return cb(null, null, subTrie, subMountInfo, subMountPath) self._maybeSetSymbols(node, subTrie, subMountInfo, subMountPath) const { trie: innerTrie, mount: innerMount, innerPath } = self._getSymbols(node) return cb(null, node, innerTrie, innerMount, innerPath) }) } } put (path, value, opts, cb) { if (typeof opts === 'function') return this.put(path, value, null, opts) path = normalize(path) const condition = putCondition(path, opts) this.trie.put(path, value, { ...opts, condition, closest: true }, (err, inserted) => { if (err && !err.mountpoint) return cb(err) else if (err) { return this._getSubtrie(path, putIntoMount) } return cb(null, inserted) }) function putIntoMount (err, trie, mountInfo) { if (err) return cb(err) const mountPath = pathToMount(path, mountInfo) return trie.put(mountPath, value, opts, (err, node) => { if (err) return cb(err) if (!node) return cb(null, null) // TODO: do we need to copy the node here? node.key = pathFromMount(node.key, mountInfo) return cb(null, node) }) } } // TODO: remove duplicate code del (path, opts, cb) { if (typeof opts === 'function') return this.del(path, null, opts) path = normalize(path) const condition = delCondition(path, opts && opts.condition) this.trie.del(path, { ...opts, condition, closest: true }, (err, deleted) => { if (err && !err.mountpoint) return cb(err) else if (err) { return this._getSubtrie(path, delFromMount) } return cb(null, deleted) }) function delFromMount (err, trie, mountInfo) { if (err) return cb(err) const mountPath = pathToMount(path, mountInfo) return trie.del(mountPath, opts, (err, node) => { if (err) return cb(err) if (!node) return cb(null, null) // TODO: do we need to copy the node here? node.key = pathFromMount(node.key, mountInfo) return cb(null, node) }) } } iterator (prefix, opts) { if (isOptions(prefix)) return this.iterator('', prefix) if (!prefix) prefix = '/' prefix = normalize(prefix) const self = this const recursive = !!(opts && opts.recursive) const noMounts = !!(opts && opts.noMounts) const gt = !!(opts && opts.gt) // gt must always be false in the trie iteration in order to discover mountpoints. if (gt) opts = { ...opts, gt: false } // Set in open. let root = null let rootInfo = null // If the iterator is currently iterating through a sub-trie, then these will be non-null. let subTrie = null let sub = null let subInfo = null return nanoiterator({ next, open }) function open (cb) { self._getSubtrie(prefix, (err, trie, mountInfo) => { if (err) return cb(err) const subPrefix = pathToMount(prefix, mountInfo) root = trie.iterator(subPrefix, opts) rootInfo = mountInfo return cb(null) }) } function next (cb) { if (sub) { return sub.next((err, node) => { if (err) return cb(err) if (!node) { sub = subInfo = subTrie = null return next(cb) } const innerPath = node.key node.key = pathFromMount(node.key, subInfo) self._maybeSetSymbols(node, subTrie, subInfo, innerPath) return prereturn(node, cb) }) } root.next((err, node) => { if (err) return cb(err) if (!node) return cb(null, null) self._maybeSetSymbols(node, self, rootInfo, node.key) if (self._isNormalNode(node) || noMounts) return prereturn(node, cb) else if (!recursive && node.key !== prefix) return prereturn(node, cb) self._getSubtrie(node.key, (err, trie, mountInfo) => { if (err) return cb(err) const subPrefix = pathToMount(node.key, mountInfo) sub = trie.iterator(subPrefix, opts) subInfo = mountInfo subTrie = trie return prereturn(node, cb) }) }) } function prereturn (node, cb) { if (gt && node.key === prefix) return next(cb) node.key = pathFromMount(node.key, rootInfo) return cb(null, node) } } list (prefix, opts, cb) { // Code duplicated from bittrie. if (typeof prefix === 'function') return this.list('', null, prefix) if (typeof opts === 'function') return this.list(prefix, null, opts) const ite = this.iterator(prefix, opts) const res = [] ite.next(function loop (err, node) { if (err) return cb(err) if (!node) return cb(null, res) res.push(node) ite.next(loop) }) } mountIterator (opts) { const memory = !!(opts && opts.memory) const recursive = !!(opts && opts.recursive) const ite = this.trie.iterator(MOUNT_PREFIX, { hidden: true }) const stack = [{ trie: this, ite, prefix: '/' }] return nanoiterator({ next }) function next (cb) { const { trie, ite, prefix } = stack[0] return ite.next((err, mountNode) => { if (err) return cb(err) if (!mountNode && stack.length === 1) return cb(null, null) if (!mountNode) { stack.shift() return next(cb) } trie._trieForMountNode(mountNode, { cached: memory }, (err, subTrie, mountInfo) => { if (err) return cb(err) if (!subTrie) return next(cb) const mountPath = p.join(prefix, mountInfo.localPath) if (recursive) { stack.unshift({ prefix: p.join(mountPath, mountInfo.remotePath), ite: subTrie.iterator(MOUNT_PREFIX, { hidden: true }), trie: subTrie }) } return cb(null, { path: mountPath, trie: subTrie }) }) }) } } listMounts (opts, cb) { if (typeof opts === 'function') return this.listMounts(null, opts) const vals = [] const ite = this.mountIterator(opts) ite.next(function onnext (err, val) { if (err) return cb(err) if (!val) return cb(null, vals) vals.push(val) return ite.next(onnext) }) } createReadStream (prefix, opts) { return toStream(this.iterator(prefix, opts)) } batch (ops, cb) { // TODO: implement } checkout (version) { return new MountableBittrie(this.chainstore, null, { ...this.opts, trie: this.trie, feed: this.feed, version: version || 1 }) } history (opts) { const self = this const ite = this.trie.history(opts) return nanoiterator({ next }) function next (cb) { ite.next((err, node) => { if (err) return cb(err) if (!node) return cb(null, null) if (self._isNormalNode(node)) return cb(null, { type: 'put', node }) return self._getSubtrie(node.key, (err, trie, mountInfo) => { if (err) return cb(err) return cb(null, { type: 'mount', info: mountInfo }) }) }) } } diff (other, prefix, opts) { if (typeof other === 'string') return this.diff(null, other, prefix) const checkout = (typeof other === 'number' || !other) ? this.checkout(other) : other if (!prefix) prefix = '/' const self = this var ite = null return nanoiterator({ next, open }) function next (cb) { var remaining = 2 ite.next((err, keyDiff) => { if (err) return cb(err) if (!keyDiff) return cb(null, null) const { left: rawLeft, right: rawRight, key } = keyDiff return updateIfMount(rawLeft, (err, left) => { if (err) return cb(err) return updateIfMount(rawRight, (err, right) => { if (err) return cb(err) return cb(null, createDiff(left, right, key)) }) }) }) } function open (cb) { self._getSubtrie(prefix, (err, trie, mountInfo) => { if (err) return cb(err) const subPrefix = pathToMount(prefix, mountInfo) ite = trie.diff(checkout.trie, pathToMount(prefix, mountInfo), opts) return cb(null) }) } function updateIfMount (node, cb) { if (!node) return process.nextTick(cb, null) if (self._isNormalNode(node)) return process.nextTick(cb, null, node) if (opts && opts.noMounts) return process.nextTick(cb, null, { info: {} }) return self._getSubtrie(node.key, (err, trie, mountInfo) => { if (err) return cb(err) return cb(null, { info: mountInfo }) }) } function createDiff (left, right, key) { const diff = { left, right, key } if (!left && right) { diff.type = !!right.info ? 'unmount' : 'del' } else { diff.type = !!left.info ? 'mount' : 'put' } return diff } } createHistoryStream (opts) { return toStream(this.history(opts)) } createDiffStream (other, prefix, opts) { return toStream(this.diff(other, prefix, opts)) } watch (path, opts, onchange) { if (typeof opts === 'function') return this.watch(path, null, opts) const self = this var destroyed = false var rootWatcher = this.trie.watch(path, onchange) var watcherKeys = (opts && opts._watcherKeys) || new Set() var watchers = [] const destroy = rootWatcher.destroy.bind(rootWatcher) rootWatcher.watchers = watchers rootWatcher.destroy = function () { if (destroyed) return destroyed = true destroy() for (let watcher of watchers) { watcher.destroy() } } createSubWatchers(err => { if (err) rootWatcher.emit('error', err) rootWatcher.emit('ready', watchers) }) return rootWatcher function createSubWatchers (cb) { self.trie.list(p.join(MOUNT_PREFIX, path), { hidden: true }, (err, mountNodes) => { if (err || destroyed) return cb(err) if (!mountNodes || !mountNodes.length) return cb(null) var readyWatchers = 0 for (let mountNode of mountNodes) { if (destroyed) return cb(null) self._trieForMountNode(mountNode, (err, trie, mountInfo) => { if (err || destroyed) return cb(err) const watcherKey = mountInfo.key.toString('hex') if (watcherKeys.has(watcherKey)) return subWatcherReady() watcherKeys.add(watcherKey) const subWatcher = trie.watch(pathToMount(path, mountInfo), { _watcherKeys: watcherKeys }, () => { onchange() }) watchers.push(subWatcher) if (trie.trie) { subWatcher.once('ready', subsubWatchers => { watchers.push.apply(watchers, subsubWatchers) return subWatcherReady() }) } else { return subWatcherReady() } }) } function subWatcherReady () { if (++readyWatchers === mountNodes.length) return cb(null) } }) } } replicate (isInitiator, opts) { const stream = new BitProtocol(isInitiator, { ...opts }) this.ready(err => { if (err) return stream.destroy(err) this.chainstore.replicate(isInitiator, { ...opts, stream }) }) return stream } } MountableBittrie.Symbols = MountableBittrie.prototype.Symbols = { TRIE: Symbol('trie'), MOUNT: Symbol('mount'), INNER_PATH: Symbol('inner-path') } module.exports = MountableBittrie function putCondition (path, opts) { const userCondition = opts && opts.condition const userClosest = opts && opts.closest return (closest, newNode, cb) => { const isWithinMount = closest && (newNode.key.startsWith(closest.key) && newNode.key !== closest.key) if (closest && (closest.flags & Flags.MOUNT) && isWithinMount) { const err = new Error('Operating on a mountpoint') err.mountpoint = true return cb(err) } if (!userCondition) return cb(null, true) if (closest && closest.key !== newNode.key && !userClosest) closest = null userCondition(closest, newNode, (err, shouldExecute) => { if (err) return cb(err) return cb(null, shouldExecute) }) } } function delCondition (path, userCondition) { return (closest, cb) => { if (closest && (closest.flags & Flags.MOUNT) && (closest.key !== path)) { const err = new Error('Operating on a mountpoint') err.mountpoint = true return cb(err) } if (!userCondition) return cb(null, true) userCondition(closest, (err, shouldExecute) => { if (err) return cb(err) return cb(null, shouldExecute) }) } } function pathToMount (path, mountInfo) { if (path.length === mountInfo.localPath.length) return '' return p.join(path.slice(mountInfo.localPath.length), mountInfo.remotePath) } function pathFromMount (path, mountInfo) { const rel = mountInfo.remotePath ? path.slice(mountInfo.remotePath.length) : path return p.join(mountInfo.localPath, rel) } function normalize (path) { path = unixify(path) return path.startsWith('/') ? path.slice(1) : path }