diff --git a/lib/db/ldb.js b/lib/db/ldb.js index 38fdca96..654e1eb4 100644 --- a/lib/db/ldb.js +++ b/lib/db/ldb.js @@ -15,50 +15,15 @@ var backends = require('./backends'); * Create a database. * @alias module:db.LDB * @param {Object} options - * @param {Boolean} options.compression - * @param {Number} options.cacheSize - * @param {Number} options.writeBufferSize - * @param {Number} options.maxOpenFiles - * @param {Boolean} options.sync - * @param {Number} options.mapSize - * @param {Boolean} options.writeMap - * @param {String} options.db - Database backend (`"leveldb"` by default). - * @param {String} options.name - Database name. - * @param {String} options.location - Database location (overrides `name`). * @returns {LowlevelUp} */ function LDB(options) { - var target = LDB.getTarget(options); - var cacheSize = options.cacheSize; + var result = LDB.getBackend(options); + var backend = result.backend; + var location = result.location; - if (!cacheSize) - cacheSize = 16 << 20; - - return new LowlevelUp(target.location, { - // Generic - createIfMissing: options.createIfMissing !== false, - errorIfExists: options.errorIfExists === true, - - // LevelDB - compression: options.compression === true, - cacheSize: cacheSize / 2 | 0, - writeBufferSize: cacheSize / 4 | 0, - maxOpenFiles: options.maxFiles || 64, - paranoidChecks: false, - memory: false, - - // LMDB - sync: options.sync || false, - mapSize: options.mapSize || 256 * (1024 << 20), - writeMap: options.writeMap || false, - noSubdir: options.noSubdir !== false, - - // Browser - bufferKeys: options.bufferKeys, - - db: target.db - }); + return new LowlevelUp(backend, location, options); } /** @@ -67,7 +32,7 @@ function LDB(options) { * @returns {Object} */ -LDB.getBackend = function getBackend(db) { +LDB.getName = function getName(db) { var name, ext; if (!db) @@ -103,7 +68,10 @@ LDB.getBackend = function getBackend(db) { break; } - return { name: name, ext: ext }; + return { + name: name, + ext: ext + }; }; /** @@ -112,20 +80,19 @@ LDB.getBackend = function getBackend(db) { * @returns {Object} */ -LDB.getTarget = function getTarget(options) { - var backend = LDB.getBackend(options.db); +LDB.getBackend = function getBackend(options) { + var result = LDB.getName(options.db); + var backend = backends.get(result.name); var location = options.location; - var db = backends.get(backend.name); if (typeof location !== 'string') { - assert(backend.name === 'memory', 'Location required.'); + assert(result.name === 'memory', 'Location required.'); location = 'memory'; } return { - db: db, - backend: backend.name, - location: location + '.' + backend.ext + backend: backend, + location: location + '.' + result.ext }; }; diff --git a/lib/db/level.js b/lib/db/level.js index d4cb8008..b80de0ae 100644 --- a/lib/db/level.js +++ b/lib/db/level.js @@ -1,3 +1,5 @@ +'use strict'; + var Level = require('level-js'); function DB(location) { diff --git a/lib/db/lowlevelup.js b/lib/db/lowlevelup.js index 56df6c42..1ecfa89d 100644 --- a/lib/db/lowlevelup.js +++ b/lib/db/lowlevelup.js @@ -8,7 +8,6 @@ 'use strict'; var assert = require('assert'); -var util = require('../utils/util'); var Lock = require('../utils/lock'); var co = require('../utils/co'); var VERSION_ERROR; @@ -23,43 +22,71 @@ var VERSION_ERROR; * * @alias module:db.LowlevelUp * @constructor - * @param {String} file - Location. - * @param {Object} options - Leveldown options. + * @param {Function} backend - Database backend. + * @param {String} location - File location. + * @param {Object?} options - Leveldown options. */ -function LowlevelUp(file, options) { +function LowlevelUp(backend, location, options) { if (!(this instanceof LowlevelUp)) - return new LowlevelUp(file, options); + return new LowlevelUp(backend, location, options); - assert(typeof file === 'string', 'Filename is required.'); - assert(options, 'Options are required.'); - assert(options.db, 'Database backend is required.'); + assert(typeof backend === 'function', 'Backend is required.'); + assert(typeof location === 'string', 'Filename is required.'); - this.options = options; - this.backend = options.db; - this.location = file; - this.bufferKeys = options.bufferKeys === true; + this.options = new LLUOptions(options); + this.backend = backend; + this.location = location; this.locker = new Lock(); this.loading = false; this.closing = false; this.loaded = false; - this.db = new options.db(file); + this.db = null; + this.binding = null; - // Stay as close to the metal as possible. - // We want to make calls to C++ directly. - while (this.db.db && this.db.db.put && this.db.db !== this.db) - this.db = this.db.db; - - this.binding = this.db; - - if (this.db.binding) - this.binding = this.db.binding; + this.init(); } +/** + * Initialize the database. + * @method + * @private + */ + +LowlevelUp.prototype.init = function init() { + var backend = this.backend; + var db = new backend(this.location); + var binding = db; + + // Stay as close to the metal as possible. + // We want to make calls to C++ directly. + while (db.db) { + // Not a database. + if (typeof db.db.put !== 'function') + break; + + // Recursive. + if (db.db === db) + break; + + // Go deeper. + db = db.db; + binding = db; + } + + // A lower-level binding. + if (db.binding) + binding = db.binding; + + this.db = db; + this.binding = binding; +}; + /** * Open the database. + * @method * @returns {Promise} */ @@ -72,22 +99,9 @@ LowlevelUp.prototype.open = co(function* open() { } }); -/** - * Close the database. - * @returns {Promise} - */ - -LowlevelUp.prototype.close = co(function* close() { - var unlock = yield this.locker.lock(); - try { - return yield this._close(); - } finally { - unlock(); - } -}); - /** * Open the database (without a lock). + * @method * @private * @returns {Promise} */ @@ -112,8 +126,24 @@ LowlevelUp.prototype._open = co(function* open() { this.loaded = true; }); +/** + * Close the database. + * @method + * @returns {Promise} + */ + +LowlevelUp.prototype.close = co(function* close() { + var unlock = yield this.locker.lock(); + try { + return yield this._close(); + } finally { + unlock(); + } +}); + /** * Close the database (without a lock). + * @method * @private * @returns {Promise} */ @@ -322,34 +352,10 @@ LowlevelUp.prototype.batch = function batch(ops) { */ LowlevelUp.prototype.iterator = function iterator(options) { - var opt; - if (!this.loaded) throw new Error('Database is closed.'); - opt = { - gte: options.gte, - lte: options.lte, - keys: options.keys !== false, - values: options.values || false, - fillCache: options.fillCache || false, - keyAsBuffer: this.bufferKeys, - valueAsBuffer: true, - reverse: options.reverse || false, - highWaterMark: options.highWaterMark || 16 * 1024 - }; - - // Workaround for a leveldown - // bug I haven't fixed yet. - if (options.limit != null) - opt.limit = options.limit; - - if (options.keyAsBuffer != null) - opt.keyAsBuffer = options.keyAsBuffer; - - assert(opt.keys || opt.values, 'Keys and/or values must be chosen.'); - - return new Iterator(this, opt); + return new Iterator(this, options); }; /** @@ -370,8 +376,8 @@ LowlevelUp.prototype.getProperty = function getProperty(name) { /** * Calculate approximate database size. - * @param {String} start - Start key. - * @param {String} end - End key. + * @param {String|Buffer} start - Start key. + * @param {String|Buffer} end - End key. * @returns {Promise} - Returns Number. */ @@ -393,6 +399,31 @@ LowlevelUp.prototype.approximateSize = function approximateSize(start, end) { }); }; +/** + * Compact range of keys. + * @param {String|Buffer} start - Start key. + * @param {String|Buffer} end - End key. + * @returns {Promise} + */ + +LowlevelUp.prototype.compactRange = function compactRange(start, end) { + var self = this; + + return new Promise(function(resolve, reject) { + if (!self.loaded) { + reject(new Error('Database is closed.')); + return; + } + + if (!self.binding.compactRange) { + reject(new Error('Cannot compact range.')); + return; + } + + self.binding.compactRange(start, end, co.wrap(resolve, reject)); + }); +}; + /** * Test whether a key exists. * @method @@ -588,8 +619,7 @@ LowlevelUp.prototype.checkVersion = co(function* checkVersion(key, version) { */ LowlevelUp.prototype.clone = co(function* clone(path) { - var options = util.merge({}, this.options); - var opt = { keys: true, values: true }; + var options = new LLUOptions(this.options); var hwm = 256 << 20; var total = 0; var tmp, batch, iter, item; @@ -600,12 +630,16 @@ LowlevelUp.prototype.clone = co(function* clone(path) { options.createIfMissing = true; options.errorIfExists = true; - tmp = new LowlevelUp(path, options); + tmp = new LowlevelUp(this.backend, path, options); yield tmp.open(); batch = tmp.batch(); - iter = this.iterator(opt); + + iter = this.iterator({ + keys: true, + values: true + }); for (;;) { item = yield iter.next(); @@ -698,6 +732,9 @@ Batch.prototype.clear = function clear() { */ function Iterator(db, options) { + options = new IteratorOptions(options); + options.keyAsBuffer = db.options.bufferKeys; + this.iter = db.db.iterator(options); } @@ -722,14 +759,14 @@ Iterator.prototype.next = function() { return; } - resolve(new KeyValue(key, value)); + resolve(new IteratorItem(key, value)); }); }); }; /** * Seek to an arbitrary key. - * @param {String|Buffer} + * @param {String|Buffer} key */ Iterator.prototype.seek = function seek(key) { @@ -748,15 +785,235 @@ Iterator.prototype.end = function end() { }); }; -/* - * Helpers +/** + * Iterator Item + * @ignore + * @constructor + * @param {String|Buffer} key + * @param {String|Buffer} value + * @property {String|Buffer} key + * @property {String|Buffer} value */ -function KeyValue(key, value) { +function IteratorItem(key, value) { this.key = key; this.value = value; } +/** + * LowlevelUp Options + * @constructor + * @ignore + * @param {Object} options + */ + +function LLUOptions(options) { + this.createIfMissing = true; + this.errorIfExists = false; + this.compression = true; + this.cacheSize = 8 << 20; + this.writeBufferSize = 4 << 20; + this.maxOpenFiles = 64; + this.maxFileSize = 2 << 20; + this.paranoidChecks = false; + this.memory = false; + this.sync = false; + this.mapSize = 256 * (1024 << 20); + this.writeMap = false; + this.noSubdir = true; + this.bufferKeys = true; + + if (options) + this.fromOptions(options); +} + +/** + * Inject properties from options. + * @private + * @param {Object} options + * @returns {LLUOptions} + */ + +LLUOptions.prototype.fromOptions = function fromOptions(options) { + assert(options, 'Options are required.'); + + if (options.createIfMissing != null) { + assert(typeof options.createIfMissing === 'boolean', + '`createIfMissing` must be a boolean.'); + this.createIfMissing = options.createIfMissing; + } + + if (options.errorIfExists != null) { + assert(typeof options.errorIfExists === 'boolean', + '`errorIfExists` must be a boolean.'); + this.errorIfExists = options.errorIfExists; + } + + if (options.compression != null) { + assert(typeof options.compression === 'boolean', + '`compression` must be a boolean.'); + this.compression = options.compression; + } + + if (options.cacheSize != null) { + assert(typeof options.cacheSize === 'number', + '`cacheSize` must be a number.'); + assert(options.cacheSize >= 0); + this.cacheSize = Math.floor(options.cacheSize / 2); + this.writeBufferSize = Math.floor(options.cacheSize / 4); + } + + if (options.maxFiles != null) { + assert(typeof options.maxFiles === 'number', + '`maxFiles` must be a number.'); + assert(options.maxFiles >= 0); + this.maxOpenFiles = options.maxFiles; + } + + if (options.maxFileSize != null) { + assert(typeof options.maxFileSize === 'number', + '`maxFileSize` must be a number.'); + assert(options.maxFileSize >= 0); + this.maxFileSize = options.maxFileSize; + } + + if (options.paranoidChecks != null) { + assert(typeof options.paranoidChecks === 'boolean', + '`paranoidChecks` must be a boolean.'); + this.paranoidChecks = options.paranoidChecks; + } + + if (options.memory != null) { + assert(typeof options.memory === 'boolean', + '`memory` must be a boolean.'); + this.memory = options.memory; + } + + if (options.sync != null) { + assert(typeof options.sync === 'boolean', + '`sync` must be a boolean.'); + this.sync = options.sync; + } + + if (options.mapSize != null) { + assert(typeof options.mapSize === 'number', + '`mapSize` must be a number.'); + assert(options.mapSize >= 0); + this.mapSize = options.mapSize; + } + + if (options.writeMap != null) { + assert(typeof options.writeMap === 'boolean', + '`writeMap` must be a boolean.'); + this.writeMap = options.writeMap; + } + + if (options.noSubdir != null) { + assert(typeof options.noSubdir === 'boolean', + '`noSubdir` must be a boolean.'); + this.noSubdir = options.noSubdir; + } + + if (options.bufferKeys != null) { + assert(typeof options.bufferKeys === 'boolean', + '`bufferKeys` must be a boolean.'); + this.bufferKeys = options.bufferKeys; + } + + return this; +}; + +/** + * Iterator Options + * @constructor + * @ignore + * @param {Object} options + */ + +function IteratorOptions(options) { + this.gte = null; + this.lte = null; + this.keys = true; + this.values = false; + this.fillCache = false; + this.keyAsBuffer = true; + this.valueAsBuffer = true; + this.reverse = false; + this.highWaterMark = 16 * 1024; + + // Note: do not add this property. + // this.limit = null; + + if (options) + this.fromOptions(options); +} + +/** + * Inject properties from options. + * @private + * @param {Object} options + * @returns {IteratorOptions} + */ + +IteratorOptions.prototype.fromOptions = function fromOptions(options) { + assert(options, 'Options are required.'); + + if (options.gte != null) { + assert(Buffer.isBuffer(options.gte) || typeof options.gte === 'string'); + this.gte = options.gte; + } + + if (options.lte != null) { + assert(Buffer.isBuffer(options.lte) || typeof options.lte === 'string'); + this.lte = options.lte; + } + + 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.fillCache != null) { + assert(typeof options.fillCache === 'boolean'); + this.fillCache = options.fillCache; + } + + 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'); + assert(options.limit >= 0); + this.limit = options.limit; + } + + if (!this.keys && !this.values) + throw new Error('Keys and/or values must be chosen.'); + + return this; +}; + +/* + * Helpers + */ + function isNotFound(err) { if (!err) return false; diff --git a/package.json b/package.json index ffbfd9e6..0c63dd2a 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ }, "optionalDependencies": { "bcoin-native": "0.0.16", - "leveldown": "1.5.0", + "leveldown": "1.7.0-0", "secp256k1": "3.2.5", "socket.io": "1.7.3", "socket.io-client": "1.7.3"