hypercore-storage
Version:
Storage engine for Hypercore
364 lines (280 loc) • 8.43 kB
JavaScript
const { Readable, getStreamError } = require('streamx')
const CloseErrorStream = require('./close-error-stream.js')
const b4a = require('b4a')
class OverlayStream extends Readable {
constructor (stream, start, end, reverse, changes, cleared) {
super()
this.start = start
this.end = end
this.reverse = reverse
this.changes = changes
this.cleared = cleared
this.change = 0
this.range = 0
this._stream = stream
this._drained = false
this._stream.on('readable', this._drainMaybe.bind(this))
this._stream.on('error', noop)
this._stream.on('close', this._onclose.bind(this))
}
_drainMaybe () {
if (this._drained === true) return
this._drained = this._onreadable()
}
_onclose () {
if (this.destroying) return
const err = getStreamError(this._stream)
if (err !== null) {
this.destroy(err)
return
}
while (this.change < this.changes.length) {
const c = this.changes[this.change++]
const key = c[0]
const value = c[1]
if (value !== null && this._inRange(key)) this.push({ key, value })
}
this.push(null)
this._stream = null
}
_onreadable () {
let data = this._stream.read()
if (data === null) return false
let drained = false
do {
if (this._push(data) === true) drained = true
data = this._stream.read()
} while (data !== null)
return drained
}
_read (cb) {
this._drained = this._onreadable()
cb(null)
}
_predestroy () {
this.stream.destroy()
}
_push (entry) {
const key = entry.key
while (this.range < this.cleared.length) {
const c = this.cleared[this.range]
// we moved past the range
if (this.reverse ? b4a.compare(key, c[0]) < 0 : b4a.compare(c[2], key) <= 0) {
this.range++
continue
}
// we didnt move past and are in, drop
if (b4a.compare(c[0], key) <= 0 && b4a.compare(key, c[2]) < 0) {
return false
}
break
}
let updated = false
while (this.change < this.changes.length) {
const c = this.changes[this.change]
const key = c[0]
const value = typeof c[1] === 'string' ? b4a.from(c[1]) : c[1]
const cmp = b4a.compare(key, entry.key)
// same value, if not deleted, return new one
if (cmp === 0) {
this.change++
if (value === null || this._inRange(key) === false) return updated
this.push({ key, value })
return true
}
// we moved past the change, push it
if (this.reverse ? cmp > 0 : cmp < 0) {
this.change++
if (value === null || this._inRange(key) === false) continue
this.push({ key, value })
updated = true
continue
}
this.push(entry)
return true
}
this.push(entry)
return true
}
_inRange (key) {
return b4a.compare(this.start, key) <= 0 && b4a.compare(key, this.end) < 0
}
}
class Overlay {
constructor () {
this.indexed = 0
this.changes = null
this.cleared = null
this.reverse = false
}
update (view, reverse) {
if (view.indexed === this.indexed) return
const changes = view.map === null ? [] : [...view.map.values()]
const cleared = view.cleared === null ? [] : view.cleared.slice(0)
const cmp = reverse ? cmpChangeReverse : cmpChange
changes.sort(cmp)
cleared.sort(cmp)
this.indexed = view.indexed
this.changes = changes
this.cleared = cleared
this.reverse = reverse
}
createStream (stream, start, end, reverse) {
return new OverlayStream(
stream,
start,
end,
reverse,
this.reverse === reverse ? this.changes : reverseArray(this.changes),
this.reverse === reverse ? this.cleared : reverseArray(this.cleared)
)
}
}
class View {
constructor () {
this.map = null
this.indexed = 0
this.changes = null
this.cleared = null
this.overlay = null
this.snap = null
this.readers = 0
}
snapshot () {
if (this._attached()) return this.snap.snapshot()
const snap = new View()
snap.map = this.map
snap.indexed = this.indexed
snap.changes = this.changes
snap.cleared = this.cleared
if (this._frozen()) return snap
this.readers++
snap.snap = this
return snap
}
readStart () {
if (this.snap !== null) this.readers++
}
readStop () {
if (this.snap !== null && --this.readers === 0) this.snap.readers--
}
size () {
return this.changes === null ? 0 : this.changes.length
}
updated () {
return this.changes === null
}
get (read, key) {
return this.changes === null ? read.get(key) : this._indexAndGet(read, key)
}
reset () {
this.indexed = 0
this.snap = this.map = this.changes = this.cleared = this.overlay = null
}
iterator (db, start, end, reverse) {
if (dbClosing(db)) return new CloseErrorStream(new Error('RocksDB session is closed'))
const stream = db.iterator({ gte: start, lt: end, reverse })
if (this.changes === null) return stream
this._index()
if (this.overlay === null) this.overlay = new Overlay()
this.overlay.update(this, reverse)
return this.overlay.createStream(stream, start, end, reverse)
}
_indexAndGet (read, key) {
this._index()
const change = this.map.get(b4a.toString(key, 'hex'))
if (change === undefined) {
return this.cleared === null
? read.get(key)
: this._readAndMaybeDrop(read, key)
}
return Promise.resolve(change[1])
}
async _readAndMaybeDrop (read, key) {
const cleared = this.cleared // in case its cleared
const value = await read.get(key)
if (value === null) return null
for (let i = 0; i < cleared.length; i++) {
const c = cleared[i]
// check if in range
if (b4a.compare(c[0], key) <= 0 && b4a.compare(key, c[2]) < 0) return null
}
return value
}
_attached () {
return this.snap !== null && this.changes === this.snap.changes
}
_frozen () {
return this.changes === null || (this.snap !== null && this.changes !== this.snap.changes)
}
_index () {
// if we are a snap and we are still attached (ie no mutations), simply copy the refs
if (this._attached()) {
this.snap._index()
this.map = this.snap.map
this.cleared = this.snap.cleared
this.indexed = this.snap.indexed
return
}
if (this.map === null) this.map = new Map()
if (this.changes.length === this.indexed) return
while (this.indexed < this.changes.length) {
const c = this.changes[this.indexed++]
if (c[2] === null) this.map.set(b4a.toString(c[0], 'hex'), c)
else this._indexRange(c)
}
}
_indexRange (range) {
const s = b4a.toString(range[0], 'hex')
const e = b4a.toString(range[2], 'hex')
for (const [key, c] of this.map) {
if (s <= key && key < e) this.map.set(key, [c[0], null, null])
}
if (this.cleared === null) this.cleared = []
this.cleared.push(range)
}
apply (changes) {
if (this.snap !== null) throw new Error('Illegal to push changes to a snapshot')
if (this.readers !== 0 && this.changes !== null) {
this.changes = this.changes.slice(0)
this.cleared = this.cleared === null ? null : this.cleared.slice(0)
this.map = this.map === null ? null : new Map([...this.map])
}
if (this.changes === null) {
this.changes = changes
return
}
for (let i = 0; i < changes.length; i++) {
this.changes.push(changes[i])
}
}
static async flush (changes, db) {
if (changes === null) return true
const w = db.write({ autoDestroy: true })
for (const [start, value, end] of changes) {
if (end !== null) w.tryDeleteRange(start, end)
else if (value !== null) w.tryPut(start, value)
else w.tryDelete(start)
}
await w.flush()
return true
}
}
module.exports = View
function cmpChange (a, b) {
const c = b4a.compare(a[0], b[0])
return c === 0 ? b4a.compare(a[2], b[2]) : c
}
function cmpChangeReverse (a, b) {
return cmpChange(b, a)
}
function noop () {}
function reverseArray (list) {
const r = new Array(list.length)
for (let i = 0; i < list.length; i++) r[r.length - 1 - i] = list[i]
return r
}
// TODO: expose from rocks instead
function dbClosing (db) {
return db._state.closing || db._index === -1
}