diff --git a/index.js b/index.js index 3af8640..47d60b7 100644 --- a/index.js +++ b/index.js @@ -1,273 +1,19 @@ 'use strict' -const { - AbstractLevel, - AbstractIterator, - AbstractKeyIterator, - AbstractValueIterator, - AbstractSnapshot -} = require('abstract-level') - +const { AbstractLevel, AbstractSnapshot } = require('abstract-level') const ModuleError = require('module-error') const createRBT = require('functional-red-black-tree') +const { MemoryEntryIterator } = require('./lib/iterator') +const { MemoryKeyIterator, MemoryValueIterator } = require('./lib/iterator') +const { MemoryClearIterator } = require('./lib/iterator') +const compare = require('./lib/compare') +const isRangeOption = require('./lib/is-range-option') -const rangeOptions = new Set(['gt', 'gte', 'lt', 'lte']) -const kNone = Symbol('none') const kTree = Symbol('tree') -const kIterator = Symbol('iterator') -const kLowerBound = Symbol('lowerBound') -const kUpperBound = Symbol('upperBound') -const kOutOfRange = Symbol('outOfRange') -const kReverse = Symbol('reverse') -const kOptions = Symbol('options') -const kTest = Symbol('test') -const kAdvance = Symbol('advance') -const kInit = Symbol('init') - -function compare (a, b) { - // Only relevant when storeEncoding is 'utf8', - // which guarantees that b is also a string. - if (typeof a === 'string') { - return a < b ? -1 : a > b ? 1 : 0 - } - - const length = Math.min(a.byteLength, b.byteLength) - - for (let i = 0; i < length; i++) { - const cmp = a[i] - b[i] - if (cmp !== 0) return cmp - } - - return a.byteLength - b.byteLength -} - -function gt (value) { - return compare(value, this[kUpperBound]) > 0 -} - -function gte (value) { - return compare(value, this[kUpperBound]) >= 0 -} - -function lt (value) { - return compare(value, this[kUpperBound]) < 0 -} - -function lte (value) { - return compare(value, this[kUpperBound]) <= 0 -} - -class MemoryIterator extends AbstractIterator { - constructor (db, options) { - super(db, options) - this[kInit](db, options) - } - - async _next () { - if (!this[kIterator].valid) return undefined - - const key = this[kIterator].key - const value = this[kIterator].value - - if (!this[kTest](key)) return undefined - - this[kIterator][this[kAdvance]]() - return [key, value] - } - - async _nextv (size, options) { - const it = this[kIterator] - const entries = [] - - while (it.valid && entries.length < size && this[kTest](it.key)) { - entries.push([it.key, it.value]) - it[this[kAdvance]]() - } - - return entries - } - - async _all (options) { - const size = this.limit - this.count - const it = this[kIterator] - const entries = [] - - while (it.valid && entries.length < size && this[kTest](it.key)) { - entries.push([it.key, it.value]) - it[this[kAdvance]]() - } - - return entries - } -} - -class MemoryKeyIterator extends AbstractKeyIterator { - constructor (db, options) { - super(db, options) - this[kInit](db, options) - } - - async _next () { - if (!this[kIterator].valid) return undefined - - const key = this[kIterator].key - if (!this[kTest](key)) return undefined - - this[kIterator][this[kAdvance]]() - return key - } - - async _nextv (size, options) { - const it = this[kIterator] - const keys = [] - - while (it.valid && keys.length < size && this[kTest](it.key)) { - keys.push(it.key) - it[this[kAdvance]]() - } - - return keys - } - - async _all (options) { - const size = this.limit - this.count - const it = this[kIterator] - const keys = [] - - while (it.valid && keys.length < size && this[kTest](it.key)) { - keys.push(it.key) - it[this[kAdvance]]() - } - - return keys - } -} - -class MemoryValueIterator extends AbstractValueIterator { - constructor (db, options) { - super(db, options) - this[kInit](db, options) - } - - async _next (options) { - if (!this[kIterator].valid) return undefined - - const key = this[kIterator].key - const value = this[kIterator].value - - if (!this[kTest](key)) return undefined - - this[kIterator][this[kAdvance]]() - return value - } - - async _nextv (size, options) { - const it = this[kIterator] - const values = [] - - while (it.valid && values.length < size && this[kTest](it.key)) { - values.push(it.value) - it[this[kAdvance]]() - } - - return values - } - - async _all (options) { - const size = this.limit - this.count - const it = this[kIterator] - const values = [] - - while (it.valid && values.length < size && this[kTest](it.key)) { - values.push(it.value) - it[this[kAdvance]]() - } - - return values - } -} - -for (const Ctor of [MemoryIterator, MemoryKeyIterator, MemoryValueIterator]) { - Ctor.prototype[kInit] = function (db, options) { - const tree = options.snapshot != null - ? options.snapshot[kTree] - : db[kTree] - - this[kReverse] = options.reverse - this[kOptions] = options - - if (!this[kReverse]) { - this[kAdvance] = 'next' - this[kLowerBound] = 'gte' in options ? options.gte : 'gt' in options ? options.gt : kNone - this[kUpperBound] = 'lte' in options ? options.lte : 'lt' in options ? options.lt : kNone - - if (this[kLowerBound] === kNone) { - this[kIterator] = tree.begin - } else if ('gte' in options) { - this[kIterator] = tree.ge(this[kLowerBound]) - } else { - this[kIterator] = tree.gt(this[kLowerBound]) - } - - if (this[kUpperBound] !== kNone) { - this[kTest] = 'lte' in options ? lte : lt - } - } else { - this[kAdvance] = 'prev' - this[kLowerBound] = 'lte' in options ? options.lte : 'lt' in options ? options.lt : kNone - this[kUpperBound] = 'gte' in options ? options.gte : 'gt' in options ? options.gt : kNone - - if (this[kLowerBound] === kNone) { - this[kIterator] = tree.end - } else if ('lte' in options) { - this[kIterator] = tree.le(this[kLowerBound]) - } else { - this[kIterator] = tree.lt(this[kLowerBound]) - } - - if (this[kUpperBound] !== kNone) { - this[kTest] = 'gte' in options ? gte : gt - } - } - } - - Ctor.prototype[kTest] = function () { - return true - } - - Ctor.prototype[kOutOfRange] = function (target) { - if (!this[kTest](target)) { - return true - } else if (this[kLowerBound] === kNone) { - return false - } else if (!this[kReverse]) { - if ('gte' in this[kOptions]) { - return compare(target, this[kLowerBound]) < 0 - } else { - return compare(target, this[kLowerBound]) <= 0 - } - } else { - if ('lte' in this[kOptions]) { - return compare(target, this[kLowerBound]) > 0 - } else { - return compare(target, this[kLowerBound]) >= 0 - } - } - } - - Ctor.prototype._seek = function (target, options) { - if (this[kOutOfRange](target)) { - this[kIterator] = this[kIterator].tree.end - this[kIterator].next() - } else if (this[kReverse]) { - this[kIterator] = this[kIterator].tree.le(target) - } else { - this[kIterator] = this[kIterator].tree.ge(target) - } - } -} class MemoryLevel extends AbstractLevel { + #tree + constructor (location, options) { // Take a dummy location argument to align with other implementations if (typeof location === 'object' && location !== null) { @@ -298,56 +44,45 @@ class MemoryLevel extends AbstractLevel { } }, forward) - this[kTree] = createRBT(compare) + this.#tree = createRBT(compare) } async _put (key, value, options) { - const it = this[kTree].find(key) + const it = this.#tree.find(key) if (it.valid) { - this[kTree] = it.update(value) + this.#tree = it.update(value) } else { - this[kTree] = this[kTree].insert(key, value) + this.#tree = this.#tree.insert(key, value) } } async _get (key, options) { - const tree = options.snapshot != null - ? options.snapshot[kTree] - : this[kTree] - - // Is undefined if not found + const tree = options.snapshot?.[kTree] ?? this.#tree return tree.get(key) } async _getMany (keys, options) { - const tree = options.snapshot != null - ? options.snapshot[kTree] - : this[kTree] - - return keys.map(getFromThis, tree) + const tree = options.snapshot?.[kTree] ?? this.#tree + return keys.map(get, tree) } async _has (key, options) { - const tree = options.snapshot != null - ? options.snapshot[kTree] - : this[kTree] + const tree = options.snapshot?.[kTree] ?? this.#tree return tree.get(key) !== undefined } async _hasMany (keys, options) { - const tree = options.snapshot != null - ? options.snapshot[kTree] - : this[kTree] + const tree = options.snapshot?.[kTree] ?? this.#tree return keys.map(has, tree) } async _del (key, options) { - this[kTree] = this[kTree].remove(key) + this.#tree = this.#tree.remove(key) } async _batch (operations, options) { - let tree = this[kTree] + let tree = this.#tree for (const op of operations) { const key = op.key @@ -360,52 +95,48 @@ class MemoryLevel extends AbstractLevel { } } - this[kTree] = tree + this.#tree = tree } async _clear (options) { - if (options.limit === -1 && !Object.keys(options).some(isRangeOption) && !options.snapshot) { + if (options.limit === -1 && !Object.keys(options).some(isRangeOption) && options.snapshot == null) { // Delete everything by creating a new empty tree. - this[kTree] = createRBT(compare) + this.#tree = createRBT(compare) return } - const iterator = this._keys({ ...options }) - const limit = iterator.limit - - let count = 0 + const tree = options.snapshot?.[kTree] ?? this.#tree + const iterator = new MemoryClearIterator(this, tree, options) - while (true) { - // TODO: add option to control "batch size" - for (let i = 0; i < 500; i++) { - if (++count > limit) return - if (!iterator[kIterator].valid) return - if (!iterator[kTest](iterator[kIterator].key)) return - - // Must also include changes made in parallel to clear() - this[kTree] = this[kTree].remove(iterator[kIterator].key) - iterator[kIterator][iterator[kAdvance]]() - } - - // Some time to breathe - await breathe() + try { + await iterator.visit(this.#clearKey) + } finally { + await iterator.close() } } + #clearKey = (key) => { + // Must also include changes made in parallel to clear() + this.#tree = this.#tree.remove(key) + } + _iterator (options) { - return new MemoryIterator(this, options) + const tree = options.snapshot?.[kTree] ?? this.#tree + return new MemoryEntryIterator(this, tree, options) } _keys (options) { - return new MemoryKeyIterator(this, options) + const tree = options.snapshot?.[kTree] ?? this.#tree + return new MemoryKeyIterator(this, tree, options) } _values (options) { - return new MemoryValueIterator(this, options) + const tree = options.snapshot?.[kTree] ?? this.#tree + return new MemoryValueIterator(this, tree, options) } _snapshot (options) { - return new MemorySnapshot(this[kTree], options) + return new MemorySnapshot(this.#tree, options) } } @@ -418,27 +149,10 @@ class MemorySnapshot extends AbstractSnapshot { exports.MemoryLevel = MemoryLevel -let breathe - -// Use setImmediate() in Node.js to allow IO in between work -if (typeof process !== 'undefined' && !process.browser && typeof global !== 'undefined' && typeof global.setImmediate === 'function') { - const setImmediate = global.setImmediate - - breathe = function () { - return new Promise(setImmediate) - } -} else { - breathe = async function () {} -} - -function getFromThis (key) { +function get (key) { return this.get(key) } -function isRangeOption (k) { - return rangeOptions.has(k) -} - function has (key) { return this.get(key) !== undefined } diff --git a/lib/breathe.js b/lib/breathe.js new file mode 100644 index 0000000..9aaf68e --- /dev/null +++ b/lib/breathe.js @@ -0,0 +1,12 @@ +'use strict' + +// Use setImmediate() in Node.js to allow IO in between work +if (typeof process !== 'undefined' && !process.browser && typeof global !== 'undefined' && typeof global.setImmediate === 'function') { + const setImmediate = global.setImmediate + + module.exports = function breathe () { + return new Promise(setImmediate) + } +} else { + module.exports = async function breathe () { } +} diff --git a/lib/compare.js b/lib/compare.js new file mode 100644 index 0000000..0da15d1 --- /dev/null +++ b/lib/compare.js @@ -0,0 +1,18 @@ +'use strict' + +module.exports = function compare (a, b) { + // Only relevant when storeEncoding is 'utf8', + // which guarantees that b is also a string. + if (typeof a === 'string') { + return a < b ? -1 : a > b ? 1 : 0 + } + + const length = Math.min(a.byteLength, b.byteLength) + + for (let i = 0; i < length; i++) { + const cmp = a[i] - b[i] + if (cmp !== 0) return cmp + } + + return a.byteLength - b.byteLength +} diff --git a/lib/is-range-option.js b/lib/is-range-option.js new file mode 100644 index 0000000..18b79e1 --- /dev/null +++ b/lib/is-range-option.js @@ -0,0 +1,7 @@ +'use strict' + +const rangeOptions = new Set(['gt', 'gte', 'lt', 'lte']) + +module.exports = function isRangeOption (k) { + return rangeOptions.has(k) +} diff --git a/lib/iterator.js b/lib/iterator.js new file mode 100644 index 0000000..2684333 --- /dev/null +++ b/lib/iterator.js @@ -0,0 +1,276 @@ +'use strict' + +const { + AbstractIterator, + AbstractKeyIterator, + AbstractValueIterator +} = require('abstract-level') + +const compare = require('./compare') +const breathe = require('./breathe') + +const kNone = Symbol('none') +const kIterator = Symbol('iterator') +const kLowerBound = Symbol('lowerBound') +const kUpperBound = Symbol('upperBound') +const kOutOfRange = Symbol('outOfRange') +const kReverse = Symbol('reverse') +const kOptions = Symbol('options') +const kTest = Symbol('test') +const kAdvance = Symbol('advance') +const kInit = Symbol('init') + +function gt (value) { + return compare(value, this[kUpperBound]) > 0 +} + +function gte (value) { + return compare(value, this[kUpperBound]) >= 0 +} + +function lt (value) { + return compare(value, this[kUpperBound]) < 0 +} + +function lte (value) { + return compare(value, this[kUpperBound]) <= 0 +} + +class MemoryEntryIterator extends AbstractIterator { + constructor (db, tree, options) { + super(db, options) + this[kInit](tree, options) + } + + async _next () { + if (!this[kIterator].valid) return undefined + + const key = this[kIterator].key + const value = this[kIterator].value + + if (!this[kTest](key)) return undefined + + this[kIterator][this[kAdvance]]() + return [key, value] + } + + async _nextv (size, options) { + const it = this[kIterator] + const entries = [] + + while (it.valid && entries.length < size && this[kTest](it.key)) { + entries.push([it.key, it.value]) + it[this[kAdvance]]() + } + + return entries + } + + async _all (options) { + const size = this.limit - this.count + const it = this[kIterator] + const entries = [] + + while (it.valid && entries.length < size && this[kTest](it.key)) { + entries.push([it.key, it.value]) + it[this[kAdvance]]() + } + + return entries + } +} + +class MemoryKeyIterator extends AbstractKeyIterator { + constructor (db, tree, options) { + super(db, options) + this[kInit](tree, options) + } + + async _next () { + if (!this[kIterator].valid) return undefined + + const key = this[kIterator].key + if (!this[kTest](key)) return undefined + + this[kIterator][this[kAdvance]]() + return key + } + + async _nextv (size, options) { + const it = this[kIterator] + const keys = [] + + while (it.valid && keys.length < size && this[kTest](it.key)) { + keys.push(it.key) + it[this[kAdvance]]() + } + + return keys + } + + async _all (options) { + const size = this.limit - this.count + const it = this[kIterator] + const keys = [] + + while (it.valid && keys.length < size && this[kTest](it.key)) { + keys.push(it.key) + it[this[kAdvance]]() + } + + return keys + } +} + +class MemoryValueIterator extends AbstractValueIterator { + constructor (db, tree, options) { + super(db, options) + this[kInit](tree, options) + } + + async _next (options) { + if (!this[kIterator].valid) return undefined + + const key = this[kIterator].key + const value = this[kIterator].value + + if (!this[kTest](key)) return undefined + + this[kIterator][this[kAdvance]]() + return value + } + + async _nextv (size, options) { + const it = this[kIterator] + const values = [] + + while (it.valid && values.length < size && this[kTest](it.key)) { + values.push(it.value) + it[this[kAdvance]]() + } + + return values + } + + async _all (options) { + const size = this.limit - this.count + const it = this[kIterator] + const values = [] + + while (it.valid && values.length < size && this[kTest](it.key)) { + values.push(it.value) + it[this[kAdvance]]() + } + + return values + } +} + +class MemoryClearIterator extends AbstractKeyIterator { + constructor (db, tree, options) { + super(db, options) + this[kInit](tree, options) + } + + // This is not an abstract-level API + async visit (visitor) { + const limit = this.limit + const it = this[kIterator] + + let count = 0 + + while (true) { + for (let i = 0; i < 500; i++) { + if (++count > limit) return + if (!it.valid || !this[kTest](it.key)) return + + visitor(it.key) + it[this[kAdvance]]() + } + + // Some time to breathe + await breathe() + } + } +} + +for (const Ctor of [MemoryEntryIterator, MemoryKeyIterator, MemoryValueIterator, MemoryClearIterator]) { + Ctor.prototype[kInit] = function (tree, options) { + this[kReverse] = options.reverse + this[kOptions] = options + + if (!this[kReverse]) { + this[kAdvance] = 'next' + this[kLowerBound] = 'gte' in options ? options.gte : 'gt' in options ? options.gt : kNone + this[kUpperBound] = 'lte' in options ? options.lte : 'lt' in options ? options.lt : kNone + + if (this[kLowerBound] === kNone) { + this[kIterator] = tree.begin + } else if ('gte' in options) { + this[kIterator] = tree.ge(this[kLowerBound]) + } else { + this[kIterator] = tree.gt(this[kLowerBound]) + } + + if (this[kUpperBound] !== kNone) { + this[kTest] = 'lte' in options ? lte : lt + } + } else { + this[kAdvance] = 'prev' + this[kLowerBound] = 'lte' in options ? options.lte : 'lt' in options ? options.lt : kNone + this[kUpperBound] = 'gte' in options ? options.gte : 'gt' in options ? options.gt : kNone + + if (this[kLowerBound] === kNone) { + this[kIterator] = tree.end + } else if ('lte' in options) { + this[kIterator] = tree.le(this[kLowerBound]) + } else { + this[kIterator] = tree.lt(this[kLowerBound]) + } + + if (this[kUpperBound] !== kNone) { + this[kTest] = 'gte' in options ? gte : gt + } + } + } + + Ctor.prototype[kTest] = function () { + return true + } + + Ctor.prototype[kOutOfRange] = function (target) { + if (!this[kTest](target)) { + return true + } else if (this[kLowerBound] === kNone) { + return false + } else if (!this[kReverse]) { + if ('gte' in this[kOptions]) { + return compare(target, this[kLowerBound]) < 0 + } else { + return compare(target, this[kLowerBound]) <= 0 + } + } else { + if ('lte' in this[kOptions]) { + return compare(target, this[kLowerBound]) > 0 + } else { + return compare(target, this[kLowerBound]) >= 0 + } + } + } + + Ctor.prototype._seek = function (target, options) { + if (this[kOutOfRange](target)) { + this[kIterator] = this[kIterator].tree.end + this[kIterator].next() + } else if (this[kReverse]) { + this[kIterator] = this[kIterator].tree.le(target) + } else { + this[kIterator] = this[kIterator].tree.ge(target) + } + } +} + +exports.MemoryEntryIterator = MemoryEntryIterator +exports.MemoryKeyIterator = MemoryKeyIterator +exports.MemoryValueIterator = MemoryValueIterator +exports.MemoryClearIterator = MemoryClearIterator diff --git a/package.json b/package.json index e979b3e..44699e4 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "files": [ "index.js", "index.d.ts", + "lib", "UPGRADING.md", "CHANGELOG.md" ],