UNPKG

levelup

Version:

Fast & simple storage - a Node.js-style LevelDB wrapper

334 lines (265 loc) 8.1 kB
'use strict' const EventEmitter = require('events').EventEmitter const inherits = require('util').inherits const DeferredLevelDOWN = require('deferred-leveldown') const IteratorStream = require('level-iterator-stream') const Batch = require('./batch') const errors = require('level-errors') const supports = require('level-supports') const catering = require('catering') const getCallback = require('./common').getCallback const getOptions = require('./common').getOptions // TODO: after we drop node 10, also use queueMicrotask() in node const nextTick = require('./next-tick') const WriteError = errors.WriteError const ReadError = errors.ReadError const NotFoundError = errors.NotFoundError const OpenError = errors.OpenError const InitializationError = errors.InitializationError function LevelUP (db, options, callback) { if (!(this instanceof LevelUP)) { return new LevelUP(db, options, callback) } let error EventEmitter.call(this) this.setMaxListeners(Infinity) if (typeof options === 'function') { callback = options options = {} } options = options || {} if (!db || typeof db !== 'object') { error = new InitializationError('First argument must be an abstract-leveldown compliant store') if (typeof callback === 'function') { return nextTick(callback, error) } throw error } if (typeof db.status !== 'string') { throw new Error('.status required, old abstract-leveldown') } this.options = getOptions(options) this._db = db this.db = null this.open(callback || ((err) => { if (err) this.emit('error', err) })) // Create manifest based on deferred-leveldown's this.supports = supports(this.db.supports, { status: true, deferredOpen: true, openCallback: true, promises: true, streams: true }) // Experimental: enrich levelup interface for (const method of Object.keys(this.supports.additionalMethods)) { if (this[method] != null) continue // Don't do this.db[method].bind() because this.db is dynamic. this[method] = function (...args) { return this.db[method](...args) } } } LevelUP.prototype.emit = EventEmitter.prototype.emit LevelUP.prototype.once = EventEmitter.prototype.once inherits(LevelUP, EventEmitter) // TODO: tests Object.defineProperty(LevelUP.prototype, 'status', { enumerable: true, get () { return this.db.status } }) // TODO: tests LevelUP.prototype.isOperational = function () { return this.db.status === 'open' || this.db.status === 'opening' } LevelUP.prototype.open = function (opts, callback) { if (typeof opts === 'function') { callback = opts opts = null } callback = catering.fromCallback(callback) if (!opts) { opts = this.options } // 1) Don't check db.status until levelup has opened, // in order for levelup events to be consistent if (this.db && this.isOpen()) { nextTick(callback, null, this) return callback.promise } if (this.db && this._isOpening()) { this.once('open', () => { callback(null, this) }) return callback.promise } // 2) Instead let deferred-leveldown handle already-open cases. // TODO: ideally though, levelup would have its own status this.db = new DeferredLevelDOWN(this._db) this.emit('opening') this.db.open(opts, (err) => { if (err) { return callback(new OpenError(err)) } this.db = this._db callback(null, this) this.emit('open') this.emit('ready') }) return callback.promise } LevelUP.prototype.close = function (callback) { callback = catering.fromCallback(callback) if (this.isOpen()) { this.db.close((err, ...rest) => { this.emit('closed') callback(err, ...rest) }) this.emit('closing') } else if (this.isClosed()) { nextTick(callback) } else if (this.db.status === 'closing') { this.once('closed', callback) } else if (this._isOpening()) { this.once('open', () => { this.close(callback) }) } return callback.promise } // TODO: remove in future major LevelUP.prototype.isOpen = function () { return this.db.status === 'open' } // TODO: remove in future major LevelUP.prototype._isOpening = function () { return this.db.status === 'opening' } // TODO: remove in future major LevelUP.prototype.isClosed = function () { return (/^clos|new/).test(this.db.status) } LevelUP.prototype.get = function (key, options, callback) { callback = getCallback(options, callback) callback = catering.fromCallback(callback) if (maybeError(this, callback)) { return callback.promise } options = getOptions(options) this.db.get(key, options, function (err, value) { if (err) { if ((/notfound/i).test(err) || err.notFound) { err = new NotFoundError('Key not found in database [' + key + ']', err) } else { err = new ReadError(err) } return callback(err) } callback(null, value) }) return callback.promise } LevelUP.prototype.getMany = function (keys, options, callback) { return this.db.getMany(keys, options, callback) } LevelUP.prototype.put = function (key, value, options, callback) { callback = getCallback(options, callback) callback = catering.fromCallback(callback) if (maybeError(this, callback)) { return callback.promise } options = getOptions(options) this.db.put(key, value, options, (err) => { if (err) { return callback(new WriteError(err)) } this.emit('put', key, value) callback() }) return callback.promise } LevelUP.prototype.del = function (key, options, callback) { callback = getCallback(options, callback) callback = catering.fromCallback(callback) if (maybeError(this, callback)) { return callback.promise } options = getOptions(options) this.db.del(key, options, (err) => { if (err) { return callback(new WriteError(err)) } this.emit('del', key) callback() }) return callback.promise } LevelUP.prototype.batch = function (arr, options, callback) { if (!arguments.length) { return new Batch(this) } if (typeof arr === 'function') callback = arr else callback = getCallback(options, callback) callback = catering.fromCallback(callback) if (maybeError(this, callback)) { return callback.promise } options = getOptions(options) this.db.batch(arr, options, (err) => { if (err) { return callback(new WriteError(err)) } this.emit('batch', arr) callback() }) return callback.promise } LevelUP.prototype.iterator = function (options) { return this.db.iterator(options) } LevelUP.prototype.clear = function (options, callback) { callback = getCallback(options, callback) options = getOptions(options) callback = catering.fromCallback(callback) if (maybeError(this, callback)) { return callback.promise } this.db.clear(options, (err) => { if (err) { return callback(new WriteError(err)) } this.emit('clear', options) callback() }) return callback.promise } LevelUP.prototype.readStream = LevelUP.prototype.createReadStream = function (options) { options = Object.assign({ keys: true, values: true }, options) if (typeof options.limit !== 'number') { options.limit = -1 } return new IteratorStream(this.db.iterator(options), options) } LevelUP.prototype.keyStream = LevelUP.prototype.createKeyStream = function (options) { return this.createReadStream(Object.assign({}, options, { keys: true, values: false })) } LevelUP.prototype.valueStream = LevelUP.prototype.createValueStream = function (options) { return this.createReadStream(Object.assign({}, options, { keys: false, values: true })) } LevelUP.prototype.toString = function () { return 'LevelUP' } LevelUP.prototype.type = 'levelup' // Expose nextTick for API parity with abstract-leveldown LevelUP.prototype._nextTick = nextTick function maybeError (db, callback) { if (!db.isOperational()) { nextTick(callback, new ReadError('Database is not open')) return true } return false } LevelUP.errors = errors module.exports = LevelUP