fcoin/lib/db/lowlevelup.js
2017-10-18 12:58:22 -07:00

1074 lines
21 KiB
JavaScript

/*!
* lowlevelup.js - LevelUP module for bcoin
* Copyright (c) 2014-2015, Fedor Indutny (MIT License)
* Copyright (c) 2014-2017, Christopher Jeffrey (MIT License).
* https://github.com/bcoin-org/bcoin
*/
'use strict';
const assert = require('assert');
const LOW = Buffer.from([0x00]);
const HIGH = Buffer.from([0xff]);
let VERSION_ERROR;
/**
* Extremely low-level version of levelup.
*
* This avoids pulling in extra deps and
* lowers memory usage.
*
* @alias module:db.LowlevelUp
* @constructor
* @param {Function} backend - Database backend.
* @param {String} location - File location.
* @param {Object?} options - Leveldown options.
*/
function LowlevelUp(backend, location, options) {
if (!(this instanceof LowlevelUp))
return new LowlevelUp(backend, location, options);
assert(typeof backend === 'function', 'Backend is required.');
assert(typeof location === 'string', 'Filename is required.');
this.options = new LLUOptions(options);
this.backend = backend;
this.location = location;
this.loading = false;
this.closing = false;
this.loaded = false;
this.binding = null;
this.leveldown = false;
this.init();
}
/**
* Initialize the database.
* @method
* @private
*/
LowlevelUp.prototype.init = function init() {
const Backend = this.backend;
let db = new Backend(this.location);
// 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;
}
// A lower-level binding.
if (db.binding) {
this.binding = db.binding;
this.leveldown = db !== db.binding;
} else {
this.binding = db;
}
};
/**
* Open the database.
* @returns {Promise}
*/
LowlevelUp.prototype.open = async function open() {
if (this.loaded)
throw new Error('Database is already open.');
assert(!this.loading);
assert(!this.closing);
this.loading = true;
try {
await this.load();
} catch (e) {
this.loading = false;
throw e;
}
this.loading = false;
this.loaded = true;
};
/**
* Close the database.
* @returns {Promise}
*/
LowlevelUp.prototype.close = async function close() {
if (!this.loaded)
throw new Error('Database is already closed.');
assert(!this.loading);
assert(!this.closing);
this.loaded = false;
this.closing = true;
try {
await this.unload();
} catch (e) {
this.loaded = true;
this.closing = false;
throw e;
}
this.closing = false;
};
/**
* Open the database.
* @private
* @returns {Promise}
*/
LowlevelUp.prototype.load = function load() {
return new Promise((resolve, reject) => {
this.binding.open(this.options, wrap(resolve, reject));
});
};
/**
* Close the database.
* @private
* @returns {Promise}
*/
LowlevelUp.prototype.unload = function unload() {
return new Promise((resolve, reject) => {
this.binding.close(wrap(resolve, reject));
});
};
/**
* Destroy the database.
* @returns {Promise}
*/
LowlevelUp.prototype.destroy = function destroy() {
return new Promise((resolve, reject) => {
if (this.loaded || this.closing) {
reject(new Error('Cannot destroy open database.'));
return;
}
if (!this.backend.destroy) {
reject(new Error('Cannot destroy (method not available).'));
return;
}
this.backend.destroy(this.location, wrap(resolve, reject));
});
};
/**
* Repair the database.
* @returns {Promise}
*/
LowlevelUp.prototype.repair = function repair() {
return new Promise((resolve, reject) => {
if (this.loaded || this.closing) {
reject(new Error('Cannot repair open database.'));
return;
}
if (!this.backend.repair) {
reject(new Error('Cannot repair (method not available).'));
return;
}
this.backend.repair(this.location, wrap(resolve, reject));
});
};
/**
* Backup the database.
* @param {String} path
* @returns {Promise}
*/
LowlevelUp.prototype.backup = function backup(path) {
if (!this.binding.backup)
return this.clone(path);
return new Promise((resolve, reject) => {
if (!this.loaded) {
reject(new Error('Database is closed.'));
return;
}
this.binding.backup(path, wrap(resolve, reject));
});
};
/**
* Retrieve a record from the database.
* @param {String|Buffer} key
* @returns {Promise} - Returns Buffer.
*/
LowlevelUp.prototype.get = function get(key) {
return new Promise((resolve, reject) => {
if (!this.loaded) {
reject(new Error('Database is closed.'));
return;
}
this.binding.get(key, (err, result) => {
if (err) {
if (isNotFound(err)) {
resolve(null);
return;
}
reject(err);
return;
}
resolve(result);
});
});
};
/**
* Store a record in the database.
* @param {String|Buffer} key
* @param {Buffer} value
* @returns {Promise}
*/
LowlevelUp.prototype.put = function put(key, value) {
if (!value)
value = LOW;
return new Promise((resolve, reject) => {
if (!this.loaded) {
reject(new Error('Database is closed.'));
return;
}
this.binding.put(key, value, wrap(resolve, reject));
});
};
/**
* Remove a record from the database.
* @param {String|Buffer} key
* @returns {Promise}
*/
LowlevelUp.prototype.del = function del(key) {
return new Promise((resolve, reject) => {
if (!this.loaded) {
reject(new Error('Database is closed.'));
return;
}
this.binding.del(key, wrap(resolve, reject));
});
};
/**
* Create an atomic batch.
* @returns {Batch}
*/
LowlevelUp.prototype.batch = function batch() {
if (!this.loaded)
throw new Error('Database is closed.');
return new Batch(this);
};
/**
* Create an iterator.
* @param {Object} options
* @returns {Iterator}
*/
LowlevelUp.prototype.iterator = function iterator(options) {
if (!this.loaded)
throw new Error('Database is closed.');
return new Iterator(this, options);
};
/**
* Get a database property.
* @param {String} name - Property name.
* @returns {String}
*/
LowlevelUp.prototype.getProperty = function getProperty(name) {
if (!this.loaded)
throw new Error('Database is closed.');
if (!this.binding.getProperty)
return '';
return this.binding.getProperty(name);
};
/**
* Calculate approximate database size.
* @param {String|Buffer} start - Start key.
* @param {String|Buffer} end - End key.
* @returns {Promise} - Returns Number.
*/
LowlevelUp.prototype.approximateSize = function approximateSize(start, end) {
return new Promise((resolve, reject) => {
if (!this.loaded) {
reject(new Error('Database is closed.'));
return;
}
if (!this.binding.approximateSize) {
reject(new Error('Cannot get size.'));
return;
}
this.binding.approximateSize(start, end, wrap(resolve, reject));
});
};
/**
* Compact range of keys.
* @param {String|Buffer|null} start - Start key.
* @param {String|Buffer|null} end - End key.
* @returns {Promise}
*/
LowlevelUp.prototype.compactRange = function compactRange(start, end) {
if (!start)
start = LOW;
if (!end)
end = HIGH;
return new Promise((resolve, reject) => {
if (!this.loaded) {
reject(new Error('Database is closed.'));
return;
}
if (!this.binding.compactRange) {
resolve();
return;
}
this.binding.compactRange(start, end, wrap(resolve, reject));
});
};
/**
* Test whether a key exists.
* @method
* @param {String} key
* @returns {Promise} - Returns Boolean.
*/
LowlevelUp.prototype.has = async function has(key) {
const value = await this.get(key);
return value != null;
};
/**
* Collect all keys from iterator options.
* @method
* @param {Object} options - Iterator options.
* @returns {Promise} - Returns Array.
*/
LowlevelUp.prototype.range = async function range(options) {
const iter = this.iterator({
gte: options.gte,
lte: options.lte,
keys: true,
values: true
});
const items = [];
await iter.each((key, value) => {
if (options.parse) {
const item = options.parse(key, value);
if (item)
items.push(item);
} else {
items.push(new IteratorItem(key, value));
}
});
return items;
};
/**
* Collect all keys from iterator options.
* @method
* @param {Object} options - Iterator options.
* @returns {Promise} - Returns Array.
*/
LowlevelUp.prototype.keys = async function keys(options) {
const iter = this.iterator({
gte: options.gte,
lte: options.lte,
keys: true,
values: false
});
const items = [];
await iter.each((key) => {
if (options.parse)
key = options.parse(key);
items.push(key);
});
return items;
};
/**
* Collect all keys from iterator options.
* @method
* @param {Object} options - Iterator options.
* @returns {Promise} - Returns Array.
*/
LowlevelUp.prototype.values = async function values(options) {
const iter = this.iterator({
gte: options.gte,
lte: options.lte,
keys: false,
values: true
});
const items = [];
await iter.each((value) => {
if (options.parse)
value = options.parse(value);
items.push(value);
});
return items;
};
/**
* Dump database (for debugging).
* @method
* @returns {Promise} - Returns Object.
*/
LowlevelUp.prototype.dump = async function dump() {
const records = Object.create(null);
const items = await this.range({
gte: LOW,
lte: HIGH
});
for (const item of items) {
const key = item.key.toString('hex');
const value = item.value.toString('hex');
records[key] = value;
}
return records;
};
/**
* Write and assert a version number for the database.
* @method
* @param {Number} version
* @returns {Promise}
*/
LowlevelUp.prototype.checkVersion = async function checkVersion(key, version) {
const data = await this.get(key);
if (!data) {
const value = Buffer.allocUnsafe(4);
value.writeUInt32LE(version, 0, true);
const batch = this.batch();
batch.put(key, value);
await batch.write();
return;
}
const num = data.readUInt32LE(0, true);
if (num !== version)
throw new Error(VERSION_ERROR);
};
/**
* Clone the database.
* @method
* @param {String} path
* @returns {Promise}
*/
LowlevelUp.prototype.clone = async function clone(path) {
if (!this.loaded)
throw new Error('Database is closed.');
const hwm = 256 << 20;
const options = new LLUOptions(this.options);
options.createIfMissing = true;
options.errorIfExists = true;
const tmp = new LowlevelUp(this.backend, path, options);
await tmp.open();
const iter = this.iterator({
keys: true,
values: true
});
let batch = tmp.batch();
let total = 0;
while (await iter.next()) {
const {key, value} = iter;
batch.put(key, value);
total += key.length + 80;
total += value.length + 80;
if (total >= hwm) {
total = 0;
try {
await batch.write();
} catch (e) {
await iter.end();
await tmp.close();
throw e;
}
batch = tmp.batch();
}
}
try {
await batch.write();
} finally {
await tmp.close();
}
};
/**
* Batch
* @constructor
* @ignore
* @param {LowlevelUp} db
*/
function Batch(db) {
this.batch = db.binding.batch();
}
/**
* Write a value to the batch.
* @param {String|Buffer} key
* @param {Buffer} value
*/
Batch.prototype.put = function put(key, value) {
if (!value)
value = LOW;
this.batch.put(key, value);
return this;
};
/**
* Delete a value from the batch.
* @param {String|Buffer} key
*/
Batch.prototype.del = function del(key) {
this.batch.del(key);
return this;
};
/**
* Write batch to database.
* @returns {Promise}
*/
Batch.prototype.write = function write() {
return new Promise((resolve, reject) => {
this.batch.write(wrap(resolve, reject));
});
};
/**
* Clear the batch.
*/
Batch.prototype.clear = function clear() {
this.batch.clear();
return this;
};
/**
* Iterator
* @constructor
* @ignore
* @param {LowlevelUp} db
* @param {Object} options
*/
function Iterator(db, options) {
this.options = new IteratorOptions(options);
this.options.keyAsBuffer = db.options.bufferKeys;
this.iter = db.binding.iterator(this.options);
this.leveldown = db.leveldown;
this.cache = [];
this.finished = false;
this.key = null;
this.value = null;
this.valid = true;
}
/**
* Clean up iterator.
* @private
*/
Iterator.prototype.cleanup = function cleanup() {
this.cache = [];
this.finished = true;
this.key = null;
this.value = null;
this.valid = false;
};
/**
* For each.
* @returns {Promise}
*/
Iterator.prototype.each = async function each(cb) {
assert(this.valid);
const {keys, values} = this.options;
while (!this.finished) {
await this.read();
while (this.cache.length > 0) {
const key = this.cache.pop();
const value = this.cache.pop();
let result = null;
try {
if (keys && values)
result = cb(key, value);
else if (keys)
result = cb(key);
else if (values)
result = cb(value);
else
assert(false);
if (result instanceof Promise)
result = await result;
} catch (e) {
await this.end();
throw e;
}
if (result === false) {
await this.end();
break;
}
}
}
};
/**
* Seek to the next key.
* @returns {Promise}
*/
Iterator.prototype.next = async function next() {
assert(this.valid);
if (!this.finished) {
if (this.cache.length === 0)
await this.read();
}
if (this.cache.length > 0) {
this.key = this.cache.pop();
this.value = this.cache.pop();
return true;
}
assert(this.finished);
this.cleanup();
return false;
};
/**
* Seek to the next key (buffer values).
* @private
* @returns {Promise}
*/
Iterator.prototype.read = function read() {
return new Promise((resolve, reject) => {
if (!this.leveldown) {
this.iter.next((err, key, value) => {
if (err) {
this.cleanup();
this.iter.end(() => reject(err));
return;
}
if (key === undefined && value === undefined) {
this.cleanup();
this.iter.end(wrap(resolve, reject));
return;
}
this.cache = [value, key];
resolve();
});
return;
}
this.iter.next((err, cache, finished) => {
if (err) {
this.cleanup();
this.iter.end(() => reject(err));
return;
}
this.cache = cache;
this.finished = finished;
resolve();
});
});
};
/**
* Seek to an arbitrary key.
* @param {String|Buffer} key
*/
Iterator.prototype.seek = function seek(key) {
assert(this.valid);
this.iter.seek(key);
};
/**
* End the iterator.
* @returns {Promise}
*/
Iterator.prototype.end = function end() {
return new Promise((resolve, reject) => {
this.cleanup();
this.iter.end(wrap(resolve, reject));
});
};
/**
* Iterator Item
* @ignore
* @constructor
* @param {String|Buffer} key
* @param {String|Buffer} value
* @property {String|Buffer} key
* @property {String|Buffer} 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.gt = null;
this.lt = 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.gt != null) {
assert(Buffer.isBuffer(options.gt) || typeof options.gt === 'string');
this.gt = options.gt;
}
if (options.lt != null) {
assert(Buffer.isBuffer(options.lt) || typeof options.lt === 'string');
this.lt = options.lt;
}
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.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;
return err.notFound
|| err.type === 'NotFoundError'
|| /not\s*found/i.test(err.message);
}
function wrap(resolve, reject) {
return function(err, result) {
if (err) {
reject(err);
return;
}
resolve(result);
};
}
VERSION_ERROR = 'Warning:'
+ ' Your database does not match the current database version.'
+ ' This is likely because the database layout or serialization'
+ ' format has changed drastically. If you want to dump your'
+ ' data, downgrade to your previous version first. If you do'
+ ' not think you should be seeing this error, post an issue on'
+ ' the repo.';
/*
* Expose
*/
module.exports = LowlevelUp;