From d4f5f7cd661799e28be1b31fd88e92dee00bc395 Mon Sep 17 00:00:00 2001 From: Christopher Jeffrey Date: Fri, 3 Mar 2017 08:49:48 -0800 Subject: [PATCH] rbt: refactor and move. --- lib/db/backends-browser.js | 6 +- lib/db/backends.js | 4 +- lib/db/index.js | 2 +- lib/db/ldb.js | 8 +- lib/db/memorydb.js | 667 ++++++++++++++++++++++++++++++++ lib/utils/index.js | 1 + lib/{db => utils}/rbt.js | 757 ++++++++++++------------------------- 7 files changed, 916 insertions(+), 529 deletions(-) create mode 100644 lib/db/memorydb.js rename lib/{db => utils}/rbt.js (50%) diff --git a/lib/db/backends-browser.js b/lib/db/backends-browser.js index ed323751..5920f6ff 100644 --- a/lib/db/backends-browser.js +++ b/lib/db/backends-browser.js @@ -7,10 +7,10 @@ 'use strict'; var level = require('./level'); -var RBT = require('./rbt'); +var MemoryDB = require('./memorydb'); exports.get = function get(name) { - if (name === 'rbt') - return RBT; + if (name === 'memory') + return MemoryDB; return level; }; diff --git a/lib/db/backends.js b/lib/db/backends.js index b3aab213..32f8f647 100644 --- a/lib/db/backends.js +++ b/lib/db/backends.js @@ -7,8 +7,8 @@ 'use strict'; exports.get = function get(name) { - if (name === 'rbt') - return require('./rbt'); + if (name === 'memory') + return require('./memorydb'); try { return require(name); diff --git a/lib/db/index.js b/lib/db/index.js index 3922ada0..95ec31bf 100644 --- a/lib/db/index.js +++ b/lib/db/index.js @@ -6,4 +6,4 @@ exports.LDB = require('./ldb'); exports.LowlevelUp = require('./lowlevelup'); -exports.RBT = require('./rbt'); +exports.MemoryDB = require('./memorydb'); diff --git a/lib/db/ldb.js b/lib/db/ldb.js index 00ee55f9..734312bd 100644 --- a/lib/db/ldb.js +++ b/lib/db/ldb.js @@ -33,7 +33,7 @@ function LDB(options) { var target = LDB.getTarget(options); var cacheSize = options.cacheSize; - if (target.backend !== 'rbt') + if (target.backend !== 'memory') util.mkdir(target.location, true); if (!cacheSize) @@ -98,7 +98,7 @@ LDB.getBackend = function getBackend(db) { case 'mem': case 'memory': case 'rbt': - name = 'rbt'; + name = 'memory'; ext = 'mem'; break; default: @@ -122,8 +122,8 @@ LDB.getTarget = function getTarget(options) { var db = backends.get(backend.name); if (typeof location !== 'string') { - assert(backend.name === 'rbt', 'Location required.'); - location = 'rbt'; + assert(backend.name === 'memory', 'Location required.'); + location = 'memory'; } return { diff --git a/lib/db/memorydb.js b/lib/db/memorydb.js new file mode 100644 index 00000000..ec5d5e85 --- /dev/null +++ b/lib/db/memorydb.js @@ -0,0 +1,667 @@ +/*! + * memorydb.js - in-memory database for bcoin + * Copyright (c) 2016-2017, Christopher Jeffrey (MIT License). + * https://github.com/bcoin-org/bcoin + */ + +'use strict'; + +var assert = require('assert'); +var util = require('../utils/util'); +var RBT = require('../utils/rbt'); +var DUMMY = new Buffer([0]); + +/** + * In memory database for bcoin + * using a red-black tree backend. + * @alias module:db.MemoryDB + * @constructor + * @param {String?} location - Phony location. + * @param {Object?} options + * @param {Function} options.compare - Comparator. + */ + +function MemoryDB(location, options) { + if (!(this instanceof MemoryDB)) + return new MemoryDB(location, options); + + if (typeof location !== 'string') { + options = location; + location = null; + } + + if (!options) + options = {}; + + this.location = location; + this.options = options; + this.tree = new RBT(util.cmp, true); +} + +/** + * Do a key lookup. + * @private + * @param {Buffer|String} key + * @returns {Buffer?} value + */ + +MemoryDB.prototype.search = function search(key) { + var node; + + if (typeof key === 'string') + key = new Buffer(key, 'utf8'); + + node = this.tree.search(key); + + if (!node) + return; + + return node.value; +}; + +/** + * Insert a record. + * @private + * @param {Buffer|String} key + * @param {Buffer} value + */ + +MemoryDB.prototype.insert = function insert(key, value) { + if (typeof key === 'string') + key = new Buffer(key, 'utf8'); + + if (typeof value === 'string') + value = new Buffer(value, 'utf8'); + + return this.tree.insert(key, value) != null; +}; + +/** + * Remove a record. + * @private + * @param {Buffer|String} key + * @returns {Boolean} + */ + +MemoryDB.prototype.remove = function remove(key) { + if (typeof key === 'string') + key = new Buffer(key, 'utf8'); + + return this.tree.remove(key) != null; +}; + +/** + * Traverse between a range of keys and collect records. + * @private + * @param {Buffer} gte + * @param {Buffer} lte + * @returns {RBTNode[]} Records. + */ + +MemoryDB.prototype.range = function range(gte, lte) { + if (typeof gte === 'string') + gte = new Buffer(gte, 'utf8'); + + if (typeof lte === 'string') + lte = new Buffer(lte, 'utf8'); + + return this.tree.range(gte, lte); +}; + +/** + * Open the database (leveldown method). + * @param {Object?} options + * @param {Function} callback + */ + +MemoryDB.prototype.open = function open(options, callback) { + if (!callback) { + callback = options; + options = null; + } + + if (!options) + options = {}; + + this.options = options; + + util.nextTick(callback); +}; + +/** + * Close the database (leveldown method). + * @param {Function} callback + */ + +MemoryDB.prototype.close = function close(callback) { + util.nextTick(callback); +}; + +/** + * Retrieve a record (leveldown method). + * @param {Buffer|String} key + * @param {Object?} options + * @param {Function} callback - Returns Bufer. + */ + +MemoryDB.prototype.get = function get(key, options, callback) { + var value, err; + + if (!callback) { + callback = options; + options = null; + } + + if (!options) + options = {}; + + value = this.search(key); + + if (!value) { + err = new Error('MemoryDB_NOTFOUND: Key not found.'); + err.notFound = true; + err.type = 'NotFoundError'; + util.nextTick(function() { + callback(err); + }); + return; + } + + if (options.asBuffer === false) + value = value.toString('utf8'); + + util.nextTick(function() { + callback(null, value); + }); +}; + +/** + * Insert a record (leveldown method). + * @param {Buffer|String} key + * @param {Buffer} value + * @param {Object?} options + * @param {Function} callback + */ + +MemoryDB.prototype.put = function put(key, value, options, callback) { + if (!callback) { + callback = options; + options = null; + } + + this.insert(key, value); + + util.nextTick(callback); +}; + +/** + * Remove a record (leveldown method). + * @param {Buffer|String} key + * @param {Object?} options + * @param {Function} callback + */ + +MemoryDB.prototype.del = function del(key, options, callback) { + if (!callback) { + callback = options; + options = null; + } + + this.remove(key); + + util.nextTick(callback); +}; + +/** + * Create an atomic batch (leveldown method). + * @see Leveldown.Batch + * @param {Object[]?} ops + * @param {Object?} options + * @param {Function} callback + */ + +MemoryDB.prototype.batch = function batch(ops, options, callback) { + var batch; + + if (!callback) { + callback = options; + options = null; + } + + batch = new Batch(this, options); + + if (ops) { + batch.ops = ops.slice(); + batch.write(callback); + return; + } + + return batch; +}; + +/** + * Create an iterator (leveldown method). + * @param {Object} options - See {Leveldown.Iterator}. + * @returns {Leveldown.Iterator}. + */ + +MemoryDB.prototype.iterator = function iterator(options) { + return new Iterator(this, options); +}; + +/** + * Get a database property (leveldown method) (NOP). + * @param {String} name - Property name. + * @returns {String} + */ + +MemoryDB.prototype.getProperty = function getProperty(name) { + return ''; +}; + +/** + * Calculate approximate database size (leveldown method). + * @param {Buffer|String} start - Start key. + * @param {Buffer|String} end - End key. + * @param {Function} callback - Returns Number. + */ + +MemoryDB.prototype.approximateSize = function approximateSize(start, end, callback) { + var items = this.range(start, end); + var size = 0; + var i, item; + + for (i = 0; i < items.length; i++) { + item = items[i]; + size += item.key.length; + size += item.value.length; + } + + util.nextTick(function() { + callback(null, size); + }); +}; + +/** + * Destroy the database (leveldown function) (NOP). + * @param {String} location + * @param {Function} callback + */ + +MemoryDB.destroy = function destroy(location, callback) { + util.nextTick(callback); +}; + +/** + * Repair the database (leveldown function) (NOP). + * @param {String} location + * @param {Function} callback + */ + +MemoryDB.repair = function repair(location, callback) { + util.nextTick(callback); +}; + +/** + * Batch + * @constructor + * @ignore + * @private + * @param {RBT} tree + * @param {Object?} options + */ + +function Batch(tree, options) { + this.options = options || {}; + this.ops = []; + this.tree = tree; + this.written = false; +} + +/** + * Insert a record. + * @param {Buffer|String} key + * @param {Buffer} value + */ + +Batch.prototype.put = function(key, value) { + assert(!this.written, 'Already written.'); + this.ops.push(new BatchOp('put', key, value)); + return this; +}; + +/** + * Remove a record. + * @param {Buffer|String} key + */ + +Batch.prototype.del = function del(key) { + assert(!this.written, 'Already written.'); + this.ops.push(new BatchOp('del', key)); + return this; +}; + +/** + * Commit the batch. + * @param {Function} callback + */ + +Batch.prototype.write = function write(callback) { + var i, op; + + if (this.written) { + util.nextTick(function() { + callback(new Error('Already written.')); + }); + return; + } + + for (i = 0; i < this.ops.length; i++) { + op = this.ops[i]; + switch (op.type) { + case 'put': + this.tree.insert(op.key, op.value); + break; + case 'del': + this.tree.remove(op.key); + break; + default: + util.nextTick(function() { + callback(new Error('Bad operation: ' + op.type)); + }); + return; + } + } + + this.ops.length = 0; + this.written = true; + + util.nextTick(callback); + + return this; +}; + +/** + * Clear batch of all ops. + */ + +Batch.prototype.clear = function clear() { + assert(!this.written, 'Already written.'); + this.ops.length = 0; + return this; +}; + +/** + * Batch Operation + * @constructor + * @ignore + * @private + * @param {String} type + * @param {Buffer} key + * @param {Buffer|null} value + */ + +function BatchOp(type, key, value) { + this.type = type; + this.key = key; + this.value = value; +} + +/** + * Iterator + * @constructor + * @ignore + * @private + * @param {RBT} db + * @param {Object?} options + */ + +function Iterator(db, options) { + this.db = db; + this.options = new IteratorOptions(options); + this.iter = null; + this.ended = false; + this.total = 0; + this.init(); +} + +/** + * Initialize the iterator. + */ + +Iterator.prototype.init = function init() { + var snapshot = this.db.tree.snapshot(); + var iter = this.db.tree.iterator(snapshot); + + if (this.options.reverse) { + if (this.options.end) { + iter.seekMax(this.options.end); + if (this.options.lt && iter.valid()) { + if (iter.compare(this.options.end) === 0) + iter.prev(); + } + } else { + iter.seekLast(); + } + } else { + if (this.options.start) { + iter.seekMin(this.options.start); + if (this.options.gt && iter.valid()) { + if (iter.compare(this.options.start) === 0) + iter.next(); + } + } else { + iter.seekFirst(); + } + } + + this.iter = iter; +}; + +/** + * Seek to the next key. + * @param {Function} callback + */ + +Iterator.prototype.next = function(callback) { + var options = this.options; + var iter = this.iter; + var key, value, result; + + if (!this.iter) { + util.nextTick(function() { + callback(new Error('Cannot call next after end.')); + }); + return; + } + + if (options.reverse) { + result = iter.prev(); + + // Stop once we hit a key below our gte key. + if (result && options.start) { + if (options.gt) { + if (iter.compare(options.start) <= 0) + result = false; + } else { + if (iter.compare(options.start) < 0) + result = false; + } + } + } else { + result = iter.next(); + + // Stop once we hit a key above our lte key. + if (result && options.end) { + if (options.lt) { + if (iter.compare(options.end) >= 0) + result = false; + } else { + if (iter.compare(options.end) > 0) + result = false; + } + } + } + + if (!result) { + this.iter = null; + util.nextTick(callback); + return; + } + + if (options.limit !== -1) { + if (this.total >= options.limit) { + this.iter = null; + util.nextTick(callback); + return; + } + this.total += 1; + } + + key = iter.key; + value = iter.value; + + if (!options.keys) + key = DUMMY; + + if (!options.values) + value = DUMMY; + + if (!options.keyAsBuffer) + key = key.toString('utf8'); + + if (!options.valueAsBuffer) + value = value.toString('utf8'); + + util.nextTick(function() { + callback(null, key, value); + }); +}; + +/** + * Seek to a key gte to `key`. + * @param {String|Buffer} key + */ + +Iterator.prototype.seek = function seek(key) { + assert(this.iter, 'Already ended.'); + + if (typeof key === 'string') + key = new Buffer(key, 'utf8'); + + if (this.options.reverse) + this.iter.seekMax(key); + else + this.iter.seekMin(key); +}; + +/** + * End the iterator. Free up snapshot. + * @param {FUnction} callback + */ + +Iterator.prototype.end = function end(callback) { + if (this.ended) { + util.nextTick(function() { + callback(new Error('Already ended.')); + }); + return; + } + + this.ended = true; + this.iter = null; + + util.nextTick(callback); +}; + +/** + * Iterator Options + * @constructor + * @ignore + * @param {Object} options + */ + +function IteratorOptions(options) { + this.keys = true; + this.values = true; + this.start = null; + this.end = null; + this.gt = false; + this.lt = false; + this.keyAsBuffer = true; + this.valueAsBuffer = true; + this.reverse = false; + this.limit = -1; + + if (options) + this.fromOptions(options); +} + +/** + * Inject properties from options. + * @private + * @param {Object} options + * @returns {IteratorOptions} + */ + +IteratorOptions.prototype.fromOptions = function fromOptions(options) { + if (options.keys != null) { + assert(typeof options.keys === 'boolean'); + this.keys = options.keys; + } + + if (options.values != null) { + assert(typeof options.values === 'boolean'); + this.values = options.values; + } + + if (options.start != null) + this.start = options.start; + + if (options.end != null) + this.end = options.end; + + if (options.gte != null) + this.start = options.gte; + + if (options.lte != null) + this.end = options.lte; + + if (options.gt != null) { + this.gt = true; + this.start = options.gt; + } + + if (options.lt != null) { + this.lt = true; + this.end = options.lt; + } + + if (options.keyAsBuffer != null) { + assert(typeof options.keyAsBuffer === 'boolean'); + this.keyAsBuffer = options.keyAsBuffer; + } + + if (options.valueAsBuffer != null) { + assert(typeof options.valueAsBuffer === 'boolean'); + this.valueAsBuffer = options.valueAsBuffer; + } + + if (options.reverse != null) { + assert(typeof options.reverse === 'boolean'); + this.reverse = options.reverse; + } + + if (options.limit != null) { + assert(typeof options.limit === 'number'); + this.limit = options.limit; + } + + return this; +}; + +/* + * Expose + */ + +module.exports = MemoryDB; diff --git a/lib/utils/index.js b/lib/utils/index.js index 64f5c7ca..c1d28d6f 100644 --- a/lib/utils/index.js +++ b/lib/utils/index.js @@ -26,6 +26,7 @@ exports.PEM = require('./pem'); exports.protobuf = require('./protobuf'); exports.ProtoWriter = exports.protobuf.ProtoWriter; exports.ProtoReader = exports.protobuf.ProtoReader; +exports.RBT = require('./rbt'); exports.BufferReader = require('./reader'); exports.StaticWriter = require('./staticwriter'); exports.util = require('./util'); diff --git a/lib/db/rbt.js b/lib/utils/rbt.js similarity index 50% rename from lib/db/rbt.js rename to lib/utils/rbt.js index 8b630a1a..7bac2329 100644 --- a/lib/db/rbt.js +++ b/lib/utils/rbt.js @@ -6,45 +6,38 @@ 'use strict'; -var util = require('../utils/util'); var assert = require('assert'); -var DUMMY = new Buffer([0]); var RED = 0; var BLACK = 1; var SENTINEL; /** * An iterative red black tree. - * Many of its options, parameters, - * and methods mimic the leveldown - * interface. - * @alias module:db.RBT + * @alias module:utils.RBT * @constructor - * @param {String?} location - Phony location. - * @param {Object?} options - * @param {Function} options.compare - Comparator. + * @param {Function} compare - Comparator. + * @param {Boolean?} unique */ -function RBT(location, options) { +function RBT(compare, unique) { if (!(this instanceof RBT)) - return new RBT(location, options); + return new RBT(compare, unique); - if (typeof location !== 'string') { - options = location; - location = null; - } + assert(typeof compare === 'function'); - if (!options) - options = {}; - - if (typeof options === 'function') - options = { compare: options }; - - this.options = options; this.root = SENTINEL; - this.compare = options.compare || util.cmp; + this.compare = compare; + this.unique = unique || false; } +/** + * Clear the tree. + */ + +RBT.prototype.reset = function reset() { + this.root = SENTINEL; +}; + /** * Do a key lookup. * @param {Buffer|String} key @@ -55,14 +48,11 @@ RBT.prototype.search = function search(key) { var current = this.root; var cmp; - if (typeof key === 'string') - key = new Buffer(key, 'utf8'); - while (!current.isNull()) { cmp = this.compare(key, current.key); if (cmp === 0) - return current.value; + return current; if (cmp < 0) current = current.left; @@ -82,16 +72,11 @@ RBT.prototype.insert = function insert(key, value) { var left = false; var parent, cmp, node; - if (typeof key === 'string') - key = new Buffer(key, 'utf8'); - - if (typeof value === 'string') - value = new Buffer(value, 'utf8'); - while (!current.isNull()) { cmp = this.compare(key, current.key); - if (cmp === 0) { + if (this.unique && cmp === 0) { + current.key = key; current.value = value; return; } @@ -112,7 +97,7 @@ RBT.prototype.insert = function insert(key, value) { if (!parent) { this.root = node; this.insertFixup(node); - return; + return node; } node.parent = parent; @@ -123,6 +108,8 @@ RBT.prototype.insert = function insert(key, value) { parent.right = node; this.insertFixup(node); + + return node; }; /** @@ -185,15 +172,12 @@ RBT.prototype.remove = function remove(key) { var current = this.root; var cmp; - if (typeof key === 'string') - key = new Buffer(key, 'utf8'); - while (!current.isNull()) { cmp = this.compare(key, current.key); if (cmp === 0) { this.removeNode(current); - return true; + return current; } if (cmp < 0) @@ -201,8 +185,6 @@ RBT.prototype.remove = function remove(key) { else current = current.right; } - - return false; }; /** @@ -440,11 +422,12 @@ RBT.prototype.predecessor = function predecessor(x) { }; /** - * Take a snapshot and return a cloned root node. + * Take a snapshot and return + * a cloned root node (iterative). * @returns {RBTNode} */ -RBT.prototype.snapshot = function snapshot() { +RBT.prototype.clone = function clone() { var current = this.root; var stack = []; var left = true; @@ -452,19 +435,23 @@ RBT.prototype.snapshot = function snapshot() { for (;;) { if (!current.isNull()) { + copy = current.clone(); + + if (parent) + copy.parent = parent; + if (left) { - copy = current.clone(); if (parent) parent.left = copy; else snapshot = copy; } else { - copy = current.clone(); if (parent) parent.right = copy; else snapshot = copy; } + stack.push(copy); parent = copy; left = true; @@ -485,285 +472,250 @@ RBT.prototype.snapshot = function snapshot() { }; /** - * Traverse the key and filter records. - * @param {Function} test - * @returns {RBTNode[]} Records. + * Take a snapshot and return + * a cloned root node (recursive). + * @returns {RBTNode} */ -RBT.prototype.traverse = function traverse(test) { - var current = this.min(this.root); - var items = []; +RBT.prototype.snapshot = function snapshot() { + var node = SENTINEL; - while (!current.isNull()) { - if (test(current)) - items.push(current.copy()); - current = this.successor(current); - } + if (this.root.isNull()) + return node; - return items; + node = this.root.clone(); + + copyLeft(node, node.left); + copyRight(node, node.right); + + return node; }; /** - * Dump all records. - * @returns {RBTNode[]} Records. + * Create an iterator. + * @param {RBTNode?} snapshot + * @returns {Iterator} */ -RBT.prototype.dump = function dump() { - return this.traverse(function() { return true; }); +RBT.prototype.iterator = function iterator(snapshot) { + return new Iterator(this, snapshot || this.root); }; /** * Traverse between a range of keys and collect records. - * @param {Buffer} gte - * @param {Buffer} lte + * @param {Buffer} min + * @param {Buffer} max * @returns {RBTNode[]} Records. */ -RBT.prototype.range = function range(gte, lte) { - var root = this.root; - var current = SENTINEL; +RBT.prototype.range = function range(min, max) { + var iter = this.iterator(); var items = []; - var cmp; - if (typeof gte === 'string') - gte = new Buffer(gte, 'utf8'); + if (min) + iter.seekMin(min); + else + iter.seekFirst(); - if (typeof lte === 'string') - lte = new Buffer(lte, 'utf8'); + while (iter.next()) { + if (max && iter.compare(max) > 0) + break; - if (gte) { - // Find the node closest to our gte key. - while (!root.isNull()) { - cmp = this.compare(gte, root.key); - - if (cmp === 0) { - current = root; - break; - } - - if (cmp < 0) { - current = root; - root = root.left; - } else { - root = root.right; - } - } - } else { - // Descend into the left subtree. - current = this.min(root); - } - - // Walk the tree in order. - while (!current.isNull()) { - if (lte) { - // Stop once we hit a key above our lte key. - cmp = this.compare(lte, current.key); - if (cmp < 0) - break; - } - - items.push(current.copy()); - current = this.successor(current); + items.push(iter.data()); } return items; }; /** - * Open the database (leveldown method). - * @param {Object?} options - * @returns {Promise} + * Iterator + * @constructor + * @ignore + * @param {RBT} tree + * @param {RBTNode} snapshot + * @property {RBT} tree + * @property {RBTNode} current + * @property {Object} key + * @property {Object} value */ -RBT.prototype.open = function open(options, callback) { - if (!callback) { - callback = options; - options = null; +function Iterator(tree, snapshot) { + this.tree = tree; + this.root = snapshot; + this.current = snapshot; + this.key = null; + this.value = null; +} + +/** + * Compare keys using tree's comparator. + * @param {Object} key + */ + +Iterator.prototype.compare = function compare(key) { + assert(this.key != null, 'No key.'); + return this.tree.compare(this.key, key); +}; + +/** + * Test whether current node is valid. + */ + +Iterator.prototype.valid = function valid() { + return !this.current.isNull(); +}; + +/** + * Seek to the root. + */ + +Iterator.prototype.reset = function reset() { + this.current = this.root; + this.key = null; + this.value = null; +}; + +/** + * Seek to the start of the tree. + */ + +Iterator.prototype.seekFirst = function seekFirst() { + this.current = this.tree.min(this.root); + this.key = this.current.key; + this.value = this.current.value; +}; + +/** + * Seek to the end of the tree. + */ + +Iterator.prototype.seekLast = function seekLast() { + this.current = this.tree.max(this.root); + this.key = this.current.key; + this.value = this.current.value; +}; + +/** + * Seek to a key from the current node (gte). + * @param {String} key + */ + +Iterator.prototype.seek = function seek(key) { + return this.seekMin(key); +}; + +/** + * Seek to a key from the current node (gte). + * @param {String} key + */ + +Iterator.prototype.seekMin = function seekMin(key) { + var root = this.current; + var current = SENTINEL; + var cmp; + + assert(key != null, 'No key passed to seek.'); + + while (!root.isNull()) { + cmp = this.tree.compare(root.key, key); + + if (cmp === 0) { + current = root; + break; + } + + if (cmp > 0) { + current = root; + root = root.left; + } else { + root = root.right; + } } - if (!options) - options = {}; - - this.options = options; - - util.nextTick(callback); + this.current = current; + this.key = current.key; + this.value = current.value; }; /** - * Close the database (leveldown method). - * @returns {Promise} + * Seek to a key from the current node (lte). + * @param {String} key */ -RBT.prototype.close = function close(callback) { - util.nextTick(callback); -}; +Iterator.prototype.seekMax = function seekMax(key) { + var root = this.current; + var current = SENTINEL; + var cmp; -/** - * Retrieve a record (leveldown method). - * @param {Buffer|String} key - * @param {Object?} options - * @returns {Promise} - Returns Buffer. - */ + assert(key != null, 'No key passed to seek.'); -RBT.prototype.get = function get(key, options, callback) { - var value, err; + while (!root.isNull()) { + cmp = this.tree.compare(root.key, key); - if (!callback) { - callback = options; - options = null; + if (cmp === 0) { + current = root; + break; + } + + if (cmp < 0) { + current = root; + root = root.right; + } else { + root = root.left; + } } - if (!options) - options = {}; + this.current = current; + this.key = current.key; + this.value = current.value; +}; - value = this.search(key); +/** + * Seek to previous node. + * @param {String} key + */ - if (!value) { - err = new Error('RBT_NOTFOUND: Key not found.'); - err.notFound = true; - err.type = 'NotFoundError'; - util.nextTick(function() { - callback(err); - }); - return; +Iterator.prototype.prev = function prev() { + if (this.current.isNull()) { + this.key = null; + this.value = null; + return false; } - if (options.asBuffer === false) - value = value.toString('utf8'); + this.key = this.current.key; + this.value = this.current.value; + this.current = this.tree.predecessor(this.current); - util.nextTick(function() { - callback(null, value); - }); + return true; }; /** - * Insert a record (leveldown method). - * @param {Buffer|String} key - * @param {Buffer} value - * @param {Object?} options - * @returns {Promise} + * Seek to next node. + * @returns {Boolean} */ -RBT.prototype.put = function put(key, value, options, callback) { - if (!callback) { - callback = options; - options = null; +Iterator.prototype.next = function next() { + if (this.current.isNull()) { + this.key = null; + this.value = null; + return false; } - this.insert(key, value); + this.key = this.current.key; + this.value = this.current.value; + this.current = this.tree.successor(this.current); - util.nextTick(callback); + return true; }; /** - * Remove a record (leveldown method). - * @param {Buffer|String} key - * @param {Object?} options - * @returns {Promise} + * Return the current key/value pair. + * @returns {RBTData} */ -RBT.prototype.del = function del(key, options, callback) { - if (!callback) { - callback = options; - options = null; - } - - this.remove(key); - - util.nextTick(callback); -}; - -/** - * Create an atomic batch (leveldown method). - * @see Leveldown.Batch - * @param {Object[]?} ops - * @param {Object?} options - * @returns {Promise} - * @returns {Leveldown.Batch} - */ - -RBT.prototype.batch = function batch(ops, options, callback) { - var batch; - - if (!callback) { - callback = options; - options = null; - } - - if (!options) - options = {}; - - batch = new Batch(this, options); - - if (ops) { - batch.ops = ops.slice(); - return batch.write(callback); - } - - return batch; -}; - -/** - * Create an iterator (leveldown method). - * @param {Object} options - See {Leveldown.Iterator}. - * @returns {Leveldown.Iterator}. - */ - -RBT.prototype.iterator = function iterator(options) { - return new Iterator(this, options); -}; - -/** - * Get a database property (leveldown method) (NOP). - * @param {String} name - Property name. - * @returns {String} - */ - -RBT.prototype.getProperty = function getProperty(name) { - return ''; -}; - -/** - * Calculate approximate database size (leveldown method). - * @param {Buffer|String} start - Start key. - * @param {Buffer|String} end - End key. - * @returns {Promise} - Returns Number. - */ - -RBT.prototype.approximateSize = function approximateSize(start, end, callback) { - var items = this.range(start, end); - var size = 0; - var i, item; - - for (i = 0; i < items.length; i++) { - item = items[i]; - size += item.key.length; - size += item.value.length; - } - - util.nextTick(function() { - callback(null, size); - }); -}; - -/** - * Destroy the database (leveldown function) (NOP). - * @param {String} location - * @returns {Promise} - */ - -RBT.destroy = function destroy(location, callback) { - util.nextTick(callback); -}; - -/** - * Repair the database (leveldown function) (NOP). - * @param {String} location - * @returns {Promise} - */ - -RBT.repair = function repair(location, callback) { - util.nextTick(callback); +Iterator.prototype.data = function data() { + assert(this.key != null, 'No data available.'); + return new RBTData(this.key, this.value); }; /** @@ -820,8 +772,8 @@ RBTNode.prototype.copy = function copy() { RBTNode.prototype.inspect = function inspect() { return { - key: stringify(this.key), - value: this.value.toString('hex'), + key: this.key, + value: this.value, color: this.color === RED ? 'red' : 'black', left: this.left, right: this.right @@ -900,266 +852,33 @@ function RBTData(key, value) { RBTData.prototype.inspect = function inspect() { return { - key: stringify(this.key), - value: this.value.toString('hex') + key: this.key, + value: this.value }; }; -/** - * Batch - * @constructor - * @ignore - * @private - * @param {RBT} tree - * @param {Object?} options - */ - -function Batch(tree, options) { - this.options = options || {}; - this.ops = []; - this.tree = tree; -} - -/** - * Insert a record. - * @param {Buffer|String} key - * @param {Buffer} value - */ - -Batch.prototype.put = function(key, value) { - assert(this.tree, 'Already written.'); - this.ops.push(new BatchOp('put', key, value)); - return this; -}; - -/** - * Remove a record. - * @param {Buffer|String} key - */ - -Batch.prototype.del = function del(key) { - assert(this.tree, 'Already written.'); - this.ops.push(new BatchOp('del', key)); - return this; -}; - -/** - * Commit the batch. - * @returns {Promise} - */ - -Batch.prototype.write = function write(callback) { - var i, op; - - if (!this.tree) { - util.nextTick(function() { - callback(new Error('Already written.')); - }); - return; - } - - for (i = 0; i < this.ops.length; i++) { - op = this.ops[i]; - switch (op.type) { - case 'put': - this.tree.insert(op.key, op.value); - break; - case 'del': - this.tree.remove(op.key); - break; - default: - util.nextTick(function() { - callback(new Error('Bad operation: ' + op.type)); - }); - return; - } - } - - this.ops.length = 0; - this.ops = null; - this.options = null; - this.tree = null; - - util.nextTick(callback); - - return this; -}; - -/** - * Clear batch of all ops. - */ - -Batch.prototype.clear = function clear() { - assert(this.tree, 'Already written.'); - this.ops.length = 0; - return this; -}; - -/** - * Batch Operation - * @constructor - * @ignore - * @private - * @param {String} type - * @param {Buffer} key - * @param {Buffer|null} value - */ - -function BatchOp(type, key, value) { - this.type = type; - this.key = key; - this.value = value; -} - -/** - * Iterator - * @constructor - * @ignore - * @private - * @param {RBT} tree - * @param {Object?} options - */ - -function Iterator(tree, options) { - if (!options) - options = {}; - - assert(!options.lt, 'LT is not implemented.'); - assert(!options.gt, 'GT is not implemented.'); - - this.options = { - keys: options.keys, - values: options.values, - gte: options.gte || options.start, - lte: options.lte || options.end, - keyAsBuffer: options.keyAsBuffer, - valueAsBuffer: options.valueAsBuffer, - reverse: options.reverse, - limit: options.limit - }; - - this.tree = tree; - this.ended = false; - this.snapshot = this.tree.range(this.options.gte, this.options.lte); - this.index = this.options.reverse ? this.snapshot.length - 1 : 0; - this.total = 0; -} - -/** - * Seek to the next key. - * @returns {Promise} - */ - -Iterator.prototype.next = function(callback) { - var item, key, value; - - if (this.ended) { - util.nextTick(function() { - callback(new Error('Cannot call next after end.')); - }); - return; - } - - if (this.options.reverse) - item = this.snapshot[this.index--]; - else - item = this.snapshot[this.index++]; - - if (this.options.limit != null) { - if (this.total++ >= this.options.limit) { - this._end(); - util.nextTick(callback); - return; - } - } - - if (!item) { - this._end(); - util.nextTick(callback); - return; - } - - key = item.key; - value = item.value; - - if (this.options.keys === false) - key = DUMMY; - - if (this.options.values === false) - value = DUMMY; - - if (this.options.keyAsBuffer === false) - key = stringify(key); - - if (this.options.valueAsBuffer === false) - value = value.toString('utf8'); - - util.nextTick(function() { - callback(null, key, value); - }); -}; - -/** - * Seek to a key gte to `key`. - * @param {String|Buffer} key - */ - -Iterator.prototype.seek = function seek(key) { - var self = this; - - assert(!this.ended, 'Already ended.'); - - if (typeof key === 'string') - key = new Buffer(key, 'utf8'); - - this.index = util.binarySearch(this.snapshot, key, function(a, b) { - return self.tree.compare(a.key, b); - }, true); -}; - -/** - * Clean up the iterator. - * @private - */ - -Iterator.prototype._end = function end() { - if (!this.tree) - return; - - this.tree = null; - this.snapshot.length = 0; - this.snapshot = null; -}; - -/** - * End the iterator. Free up snapshot. - * @param {Buffer} callback - */ - -Iterator.prototype.end = function end(callback) { - if (this.ended) { - util.nextTick(function() { - callback(new Error('Already ended.')); - }); - return; - } - - this.ended = true; - this._end(); - - util.nextTick(callback); -}; - /* * Helpers */ SENTINEL = new RBTSentinel(); -function stringify(value) { - if (Buffer.isBuffer(value)) - return value.toString('utf8'); - return value; +function copyLeft(parent, node) { + if (!node.isNull()) { + parent.left = node.clone(); + parent.left.parent = parent; + copyLeft(parent.left, node.left); + copyRight(parent.left, node.right); + } +} + +function copyRight(parent, node) { + if (!node.isNull()) { + parent.right = node.clone(); + parent.right.parent = parent; + copyLeft(parent.right, node.left); + copyRight(parent.right, node.right); + } } /*