UNPKG

@telios/nebula

Version:

Real-time distributed file and data storage.

677 lines (546 loc) 18.4 kB
const { rmdir } = require('fs').promises const Hypercore = require('hypercore') const EventEmitter = require('events') const Autobase = require('autobase') const Autodeebee = require('hyperbeedeebee/autodeebee') const Hyperswarm = require('hyperswarm') const HyperFTS = require('./hyper-fts') const { DB } = require('hyperbeedeebee') const BSON = require('bson') const debounce = require('debounce') class Database extends EventEmitter { constructor(storage, opts = {}) { super() const key = opts.peerPubKey ? opts.peerPubKey : null this.storageName = opts.storageName this.opts = opts this.keyPair = opts.keyPair this.autobee = null this.bee = null this.metaAutobee = null this.metadb = null this.acl = opts.acl this.peerPubKey = key this.encryptionKey = opts.encryptionKey this.storage = typeof storage === 'string' ? `${storage}/Database` : storage this.storageIsString = typeof storage === 'string' ? true : false this.joinSwarm = typeof opts.joinSwarm === 'boolean' ? opts.joinSwarm : true this.storageMaxBytes = opts.storageMaxBytes this.stat = opts.stat this.localDB = opts.localDB this.fileStatPath = opts.fileStatPath this.blind = opts.blind this.syncingCoreCount = 0 this.broadcast = opts.broadcast this._inputs = new Map() this._dbVersion = opts.dbVersion this.localInput = { key: Buffer.alloc(32) } this._statTimeout = null // Init local Autobase if(!this.blind) { this.localInput = new Hypercore( this.storageIsString ? `${this.storage}/local-writer` : this.storage, null, { encryptionKey: this.encryptionKey } ) this.localOutput = new Hypercore( this.storageIsString ? `${this.storage}/local-index` : this.storage, null, { encryptionKey: this.encryptionKey } ) const base1 = new Autobase({ inputs: [this.localInput], localOutput: this.localOutput, localInput: this.localInput }) this.autobee = new Autodeebee(base1) } // Init Meta Autobase if(this.peerPubKey) { this.remoteMetaCore = new Hypercore(this.storageIsString ? `${this.storage}/meta-remote` : this.storage, this.peerPubKey) } this.localMetaCore = new Hypercore(this.storageIsString ? `${this.storage}/meta-local` : this.storage) this.metaIndex = new Hypercore(this.storageIsString ? `${this.storage}/meta-index` : this.storage) const base2 = new Autobase({ inputs: [this.localMetaCore], localOutput: this.metaIndex, localInput: this.localMetaCore }) try { this.metaAutobee = new Autodeebee(base2) } catch(e) { } if(this.peerPubKey) { this.addInput(this.remoteMetaCore, 'meta') } this.collections = {} this.connections = [] this.coresLocal = new Map() this.coresRemote = new Map() // Init local search index if(opts.fts) { this.fts = new HyperFTS(this.storageIsString ? `${this.storage}/fts` : this.storage, this.encryptionKey) } } async ready() { if(this.localDB) { this.lastWriterSeq = this.localDB.get('lastWSeq') } this.connections = [] // reset connections if(this.opts.fts && !this.blind) { await this.fts.ready() } if(!this.blind) { this.autobeeVersion = this.autobee.version() await this.autobee.ready() await this._joinSwarm(this.localInput, { server: true, client: true }) if(!this.bee) { this.bee = new DB(this.autobee) } } this.metaAutobeeVersion = this.metaAutobee.version() await this.metaAutobee.ready() await this._joinSwarm(this.localMetaCore, { server: true, client: true }) if(!this.metadb) { this.mdb = new DB(this.metaAutobee) this.metadb = await this.mdb.collection('metadb') } if(this.peerPubKey) { try { this._handleCoreSyncStatus(1) let remoteMetaDidSync = false this.remoteMetaCore.on('append', debounce(async () => { if(!remoteMetaDidSync) { this._handleCoreSyncStatus(-1) remoteMetaDidSync = true } await this._getDiff(this.metaAutobee, this.metaAutobeeVersion) if(this.storageMaxBytes) await this._updateStatBytes() this.metaAutobeeVersion = this.metaAutobee.version() }, 500)) await this._joinSwarm(this.remoteMetaCore, { server: true, client: true }) // Download blocks from remote peer this.remoteMetaCore.download({ start: 0, end: -1 }) } catch(err) { // No results } } if(this.broadcast) { const peerInfo = { __version: this._dbVersion, blacklisted: false, peerPubKey: this.keyPair.publicKey.toString('hex'), blind: this.blind, cores : { writer: !this.blind ? this.localInput.key.toString('hex') : null, meta: this.localMetaCore.key.toString('hex') } } try { const doc = await this.metadb.findOne({ peerPubKey: this.keyPair.publicKey.toString('hex')}) } catch(err) { await this.metadb.insert(peerInfo) } } try { const docs = await this.metadb.find() for await(const doc of docs) { if(doc.cores && !doc.blacklisted && doc.__version === this._dbVersion) { const peer = { blind: doc.blind, peerPubKey: doc.peerPubKey, ...doc.cores } await this.addRemotePeer(peer) } } } catch(err) { console.log(err) } this.opened = true } async addRemotePeer(peer) { if(!this._inputs.get(peer.writer) && !this._inputs.get(peer.meta) && peer.peerPubKey !== this.keyPair.publicKey.toString('hex')) { if(peer.meta && !peer.writer) { this._handleCoreSyncStatus(1) } else { this._handleCoreSyncStatus(2) } if(peer.writer) { let peerWriterDidSync = false const peerWriter = new Hypercore( this.storageIsString ? `${this.storage}/peers/${peer.writer}` : this.storage, peer.writer, { encryptionKey: this.encryptionKey } ) if(!this.blind) { await this.addInput(peerWriter, 'autobee') } peerWriter.update().then(() => { peerWriter.on('append', debounce(async () => { if(!peerWriterDidSync) this._handleCoreSyncStatus(-1) peerWriterDidSync = true if(!this.blind) { this._getDiff(this.autobee, this.autobeeVersion, 'autob', peerWriter.length) } else { this.emit('collection-update') } if(this.storageMaxBytes) this._updateStatBytes() }, 500)) }) await this._joinSwarm(peerWriter, { server: true, client: true, peer }) this._inputs.set(peer.writer, peerWriter) // Download blocks from remote peer peerWriter.download({ start: 0, end: -1 }) } if(peer.meta) { let peerMetaDidSync = false const peerMeta = new Hypercore( this.storageIsString ? `${this.storage}/peers/${peer.meta}` : this.storage, peer.meta ) await this.addInput(peerMeta, 'meta') peerMeta.on('append', debounce(async () => { if(!peerMetaDidSync) this._handleCoreSyncStatus(-1) peerMetaDidSync = true await this._getDiff(this.metaAutobee, this.metaAutobeeVersion, 'meta') if(this.storageMaxBytes) this._updateStatBytes() this.metaAutobeeVersion = this.metaAutobee.version() }, 500)) await this._joinSwarm(peerMeta, { server: true, client: true, peer }) this._inputs.set(peer.meta, peerMeta) // Download blocks from remote peer peerMeta.download({ start: 0, end: -1 }) } } } async removeRemotePeer(peer) { try { if(peer.peerPubKey !== this.keyPair.publicKey.toString('hex')) { if(!this.blind && !peer.blind) { const core = this._inputs.get(peer.writer) if(core) { await this.autobee.removeInput(core) await core.close() // Remove Hypercores from disk await rmdir(`${this.storage}/peers/${peer.writer}`, { recursive: true, force: true, }) } } const metaCore = this._inputs.get(peer.meta) if(metaCore) { await this.metaAutobee.removeInput(metaCore) await metaCore.close() // Remove Hypercores from disk await rmdir(`${this.storage}/peers/${peer.meta}`, { recursive: true, force: true, }) } } } catch(e) {} } async addInput(core, type) { if(!type) return if(!core.key) { await core.ready() } if(type === 'autobee') { await this.autobee.addInput(core) } if(type === 'meta') { await this.metaAutobee.addInput(core) } } async collection(name) { const _collection = await this.bee.collection(name) const self = this const collection = new Proxy(_collection, { get (target, prop) { if(prop === 'insert') { return async (doc, opts) => { const _doc = {...doc, author: self.keyPair.publicKey.toString('hex') } return await _collection.insert(_doc) } } if(prop === 'update') { return async (query, data, opts) => { let _data = {...data } _data = {..._data, author: self.keyPair.publicKey.toString('hex') } return await _collection.update(query, _data, opts) } } if(prop === 'remove') { return async (query) => { if(self.fts) { await self.fts.deIndex({ db:_collection, name, query }) } await _collection.delete(query) } } return _collection[prop] } }) collection.ftsIndex = async (props, docs) => { if(!this.opts.fts) throw('Full text search is currently disabled because the option was set to false') await this.fts.index({ name, props, docs }) } collection.search = async (query, opts) => { if(!this.opts.fts) throw('Full text search is currently disabled because the option was set to false') return this.fts.search({ db:collection, name, query, opts }) } this.collections[name] = collection return collection } // TODO: Figure out how to multiplex these connections async _joinSwarm(core, { server, client, peer }) { if(!this.storageMaxBytes || this.stat.total_bytes < this.storageMaxBytes) { const swarm = new Hyperswarm() try { await core.ready() } catch(err) { console.log(err) } if(this.joinSwarm) { let connected = false try { swarm.on('connection', async (socket, info) => { connected = true socket.on('error', err => {}) if(peer && peer.peerPubKey) { this.emit('peer-connected', peer) socket.on('close', () => { this.emit('peer-disconnected', peer) }) } socket.pipe(core.replicate(info.client)).pipe(socket) }) const topic = core.discoveryKey; const discovery = swarm.join(topic, { server, client }) this.connections.push(swarm) if(server) { discovery.flushed().then(() => { this.emit('connected') }) } swarm.flush().then(() => { this.emit('connected') }) // Refresh if no connection has been made within 10s const refreshInt = setInterval(() => { if(!connected) { discovery.refresh({ client, server }) } else { clearInterval(refreshInt) } }, 10000) } catch(e) { this.emit('disconnected') } } } } async _leaveSwarm() { for await(const conn of this.connections) { await conn.destroy() } } async _updateStatBytes(fileBytes) { return new Promise(async (resolve, reject) => { let totalBytes = 0 if(this._statTimeout && !fileBytes) { return resolve(this.stat.total_bytes) } this._statTimeout = setTimeout(() => { clearTimeout(this._statTimeout) this._statTimeout = null }, 200) totalBytes += this.metaIndex.byteLength totalBytes += this.localMetaCore.byteLength if(!this.blind) { totalBytes += this.localInput.byteLength totalBytes += this.localOutput.byteLength } if(this._inputs.size) { for (const [key, value] of this._inputs.entries()) { const core = this._inputs.get(key) totalBytes += core.byteLength } } this.stat.core_bytes = totalBytes if(fileBytes) { this.stat.file_bytes += fileBytes this.stat.total_bytes = totalBytes + this.stat.file_bytes } this.localDB.put('stat', { ...this.stat }) if(this.storageMaxBytes && this.stat.total_bytes >= this.storageMaxBytes) { // Shut down replication await this._leaveSwarm() } return resolve(totalBytes) }) } async close() { if(this.opts.fts) { await this.fts.close() } if(this.autobee) { await this.autobee.close() } await this.metaAutobee.close() for await(const conn of this.connections) { await conn.destroy() } if(this.remoteMetaCore) { for (const session of this.remoteMetaCore.sessions) { if(session) { await session.close() } } } if(this.metaIndex) { for (const session of this.metaIndex.sessions) { if(session) { await session.close() } } } if(this.localOutput) { for (const session of this.localOutput.sessions) { if(session) { await session.close() } } } // Only way found to force-close autobase and prevent file lock errors when // trying to re-instantiate try { await this.remoteMetaCore.sessions[0].close() while(this.remoteMetaCore.sessions[0].opened) { await this.remoteMetaCore.sessions[0].close() } } catch(e) {} try { await this.metaIndex.sessions[0].close() while(this.metaIndex.sessions[0].opened) { await this.metaIndex.sessions[0].close() } } catch(e) {} try { await this.localOutput.sessions[0].close() while(this.localOutput.sessions[0].opened) { await this.localOutput.sessions[0].close() } } catch(e) {} if(this._inputs.size) { for await(const [key, value] of this._inputs.entries()) { const core = this._inputs.get(key) if(core && core.opened) { await core.close() } try { await core.sessions[0].close() while(core.sessions[0].opened) { await core.sessions[0].close() } } catch(e) { } } } this.metadb = null this.mdb = null this.bee = null this._inputs = new Map() this._inputs = [] this.collections = {} this.connections = [] this.coresLocal = new Map() this.coresRemote = new Map() this.recordsSet = new Set() this.removeAllListeners() this.opened = false } async _getDiff(bee, version, type, length) { let diffStream = bee.createDiffStream(version) let lastSeq = 0 for await(const data of diffStream) { let node // New Record if(data && data.left && data.left.key.toString().indexOf('\x00doc') > -1 && !data.right) { node = await this._buildNode(data.left, 'create') } // Recored Updated if(data && data.left && data.left.key.toString().indexOf('\x00doc') > -1 && data.right && data.right.key.toString().indexOf('\x00doc') > -1) { node = await this._buildNode(data.left, 'update') } // Deleted Record if(data && !data.left && data.right && data.right.key.toString().indexOf('\x00doc') > -1) { node = await this._buildNode(data.right, 'del') } if(data && data.right && data.right.key.toString().indexOf('\x00doc') > -1 || data && data.left && data.left.key.toString().indexOf('\x00doc') > -1) { if(type === 'autob') { lastSeq = node.seq this.autobeeVersion = bee.version() } } if(node && node.collection === 'metadb' || node && node.collection !== 'metadb' && !this.lastWriterSeq || node && node.collection !== 'metadb' && this.lastWriterSeq && this.lastWriterSeq < node.seq ) { this.emit('collection-update', node) } } if(this.localDB) { this.localDB.put('lastWSeq', lastSeq) } diffStream = null } async _buildNode(data, event) { const _data = BSON.deserialize(data.value) const collection = data.key.toString().split('\x00doc')[0] let node = { collection, type: event, value: _data, seq: data.seq } if(collection === 'metadb' && _data.cores && _data.__version === this._dbVersion) { const peer = { blind: _data.blind, peerPubKey: _data.peerPubKey, ..._data.cores } if(_data.blacklisted) { await this.removeRemotePeer(peer) } else { const peerInfo = { __version: this._dbVersion, blacklisted: false, peerPubKey: peer.peerPubKey, blind: peer.blind, cores : { writer: _data.cores.writer, meta: _data.cores.meta } } await this.metadb.update({peerPubKey: peer.peerPubKey}, peerInfo, { upsert: true }) await this.addRemotePeer(peer) } } return node } _handleCoreSyncStatus(count) { this.syncingCoreCount += count if(this.syncingCoreCount === 0) { this.emit('remote-cores-downloaded') } } } module.exports = Database