'use strict' const { supports } = require('level-supports') const { Transcoder } = require('level-transcoder') const { EventEmitter } = require('events') const { fromCallback } = require('catering') const ModuleError = require('module-error') const { AbstractIterator } = require('./abstract-iterator') const { DefaultKeyIterator, DefaultValueIterator } = require('./lib/default-kv-iterator') const { DeferredIterator, DeferredKeyIterator, DeferredValueIterator } = require('./lib/deferred-iterator') const { DefaultChainedBatch } = require('./lib/default-chained-batch') const { getCallback, getOptions } = require('./lib/common') const rangeOptions = require('./lib/range-options') const kPromise = Symbol('promise') const kLanded = Symbol('landed') const kResources = Symbol('resources') const kCloseResources = Symbol('closeResources') const kOperations = Symbol('operations') const kUndefer = Symbol('undefer') const kDeferOpen = Symbol('deferOpen') const kOptions = Symbol('options') const kStatus = Symbol('status') const kDefaultOptions = Symbol('defaultOptions') const kTranscoder = Symbol('transcoder') const kKeyEncoding = Symbol('keyEncoding') const kValueEncoding = Symbol('valueEncoding') const noop = () => {} class AbstractLevel extends EventEmitter { constructor (manifest, options) { super() if (typeof manifest !== 'object' || manifest === null) { throw new TypeError("The first argument 'manifest' must be an object") } options = getOptions(options) const { keyEncoding, valueEncoding, passive, ...forward } = options this[kResources] = new Set() this[kOperations] = [] this[kDeferOpen] = true this[kOptions] = forward this[kStatus] = 'opening' this.supports = supports(manifest, { status: true, promises: true, clear: true, getMany: true, deferredOpen: true, // TODO (next major): add seek snapshots: manifest.snapshots !== false, permanence: manifest.permanence !== false, // TODO: remove from level-supports because it's always supported keyIterator: true, valueIterator: true, iteratorNextv: true, iteratorAll: true, encodings: manifest.encodings || {}, events: Object.assign({}, manifest.events, { opening: true, open: true, closing: true, closed: true, put: true, del: true, batch: true, clear: true }) }) this[kTranscoder] = new Transcoder(formats(this)) this[kKeyEncoding] = this[kTranscoder].encoding(keyEncoding || 'utf8') this[kValueEncoding] = this[kTranscoder].encoding(valueEncoding || 'utf8') // Add custom and transcoder encodings to manifest for (const encoding of this[kTranscoder].encodings()) { if (!this.supports.encodings[encoding.commonName]) { this.supports.encodings[encoding.commonName] = true } } this[kDefaultOptions] = { empty: Object.freeze({}), entry: Object.freeze({ keyEncoding: this[kKeyEncoding].commonName, valueEncoding: this[kValueEncoding].commonName }), key: Object.freeze({ keyEncoding: this[kKeyEncoding].commonName }) } // Let subclass finish its constructor this.nextTick(() => { if (this[kDeferOpen]) { this.open({ passive: false }, noop) } }) } get status () { return this[kStatus] } keyEncoding (encoding) { return this[kTranscoder].encoding(encoding != null ? encoding : this[kKeyEncoding]) } valueEncoding (encoding) { return this[kTranscoder].encoding(encoding != null ? encoding : this[kValueEncoding]) } open (options, callback) { callback = getCallback(options, callback) callback = fromCallback(callback, kPromise) options = { ...this[kOptions], ...getOptions(options) } options.createIfMissing = options.createIfMissing !== false options.errorIfExists = !!options.errorIfExists const maybeOpened = (err) => { if (this[kStatus] === 'closing' || this[kStatus] === 'opening') { // Wait until pending state changes are done this.once(kLanded, err ? () => maybeOpened(err) : maybeOpened) } else if (this[kStatus] !== 'open') { callback(new ModuleError('Database is not open', { code: 'LEVEL_DATABASE_NOT_OPEN', cause: err })) } else { callback() } } if (options.passive) { if (this[kStatus] === 'opening') { this.once(kLanded, maybeOpened) } else { this.nextTick(maybeOpened) } } else if (this[kStatus] === 'closed' || this[kDeferOpen]) { this[kDeferOpen] = false this[kStatus] = 'opening' this.emit('opening') this._open(options, (err) => { if (err) { this[kStatus] = 'closed' // Resources must be safe to close in any db state this[kCloseResources](() => { this.emit(kLanded) maybeOpened(err) }) this[kUndefer]() return } this[kStatus] = 'open' this[kUndefer]() this.emit(kLanded) // Only emit public event if pending state changes are done if (this[kStatus] === 'open') this.emit('open') // TODO (next major): remove this alias if (this[kStatus] === 'open') this.emit('ready') maybeOpened() }) } else if (this[kStatus] === 'open') { this.nextTick(maybeOpened) } else { this.once(kLanded, () => this.open(options, callback)) } return callback[kPromise] } _open (options, callback) { this.nextTick(callback) } close (callback) { callback = fromCallback(callback, kPromise) const maybeClosed = (err) => { if (this[kStatus] === 'opening' || this[kStatus] === 'closing') { // Wait until pending state changes are done this.once(kLanded, err ? maybeClosed(err) : maybeClosed) } else if (this[kStatus] !== 'closed') { callback(new ModuleError('Database is not closed', { code: 'LEVEL_DATABASE_NOT_CLOSED', cause: err })) } else { callback() } } if (this[kStatus] === 'open') { this[kStatus] = 'closing' this.emit('closing') const cancel = (err) => { this[kStatus] = 'open' this[kUndefer]() this.emit(kLanded) maybeClosed(err) } this[kCloseResources](() => { this._close((err) => { if (err) return cancel(err) this[kStatus] = 'closed' this[kUndefer]() this.emit(kLanded) // Only emit public event if pending state changes are done if (this[kStatus] === 'closed') this.emit('closed') maybeClosed() }) }) } else if (this[kStatus] === 'closed') { this.nextTick(maybeClosed) } else { this.once(kLanded, () => this.close(callback)) } return callback[kPromise] } [kCloseResources] (callback) { if (this[kResources].size === 0) { return this.nextTick(callback) } let pending = this[kResources].size let sync = true const next = () => { if (--pending === 0) { // We don't have tests for generic resources, so dezalgo if (sync) this.nextTick(callback) else callback() } } // In parallel so that all resources know they are closed for (const resource of this[kResources]) { resource.close(next) } sync = false this[kResources].clear() } _close (callback) { this.nextTick(callback) } get (key, options, callback) { callback = getCallback(options, callback) callback = fromCallback(callback, kPromise) options = getOptions(options, this[kDefaultOptions].entry) if (this[kStatus] === 'opening') { this.defer(() => this.get(key, options, callback)) return callback[kPromise] } if (maybeError(this, callback)) { return callback[kPromise] } const err = this._checkKey(key) if (err) { this.nextTick(callback, err) return callback[kPromise] } const keyEncoding = this.keyEncoding(options.keyEncoding) const valueEncoding = this.valueEncoding(options.valueEncoding) const keyFormat = keyEncoding.format const valueFormat = valueEncoding.format // Forward encoding options to the underlying store if (options.keyEncoding !== keyFormat || options.valueEncoding !== valueFormat) { // Avoid spread operator because of https://bugs.chromium.org/p/chromium/issues/detail?id=1204540 options = Object.assign({}, options, { keyEncoding: keyFormat, valueEncoding: valueFormat }) } this._get(this.prefixKey(keyEncoding.encode(key), keyFormat), options, (err, value) => { if (err) { // Normalize not found error for backwards compatibility with abstract-leveldown and level(up) if (err.code === 'LEVEL_NOT_FOUND' || err.notFound || /NotFound/i.test(err)) { if (!err.code) err.code = 'LEVEL_NOT_FOUND' // Preferred way going forward if (!err.notFound) err.notFound = true // Same as level-errors if (!err.status) err.status = 404 // Same as level-errors } return callback(err) } try { value = valueEncoding.decode(value) } catch (err) { return callback(new ModuleError('Could not decode value', { code: 'LEVEL_DECODE_ERROR', cause: err })) } callback(null, value) }) return callback[kPromise] } _get (key, options, callback) { this.nextTick(callback, new Error('NotFound')) } getMany (keys, options, callback) { callback = getCallback(options, callback) callback = fromCallback(callback, kPromise) options = getOptions(options, this[kDefaultOptions].entry) if (this[kStatus] === 'opening') { this.defer(() => this.getMany(keys, options, callback)) return callback[kPromise] } if (maybeError(this, callback)) { return callback[kPromise] } if (!Array.isArray(keys)) { this.nextTick(callback, new TypeError("The first argument 'keys' must be an array")) return callback[kPromise] } if (keys.length === 0) { this.nextTick(callback, null, []) return callback[kPromise] } const keyEncoding = this.keyEncoding(options.keyEncoding) const valueEncoding = this.valueEncoding(options.valueEncoding) const keyFormat = keyEncoding.format const valueFormat = valueEncoding.format // Forward encoding options if (options.keyEncoding !== keyFormat || options.valueEncoding !== valueFormat) { options = Object.assign({}, options, { keyEncoding: keyFormat, valueEncoding: valueFormat }) } const mappedKeys = new Array(keys.length) for (let i = 0; i < keys.length; i++) { const key = keys[i] const err = this._checkKey(key) if (err) { this.nextTick(callback, err) return callback[kPromise] } mappedKeys[i] = this.prefixKey(keyEncoding.encode(key), keyFormat) } this._getMany(mappedKeys, options, (err, values) => { if (err) return callback(err) try { for (let i = 0; i < values.length; i++) { if (values[i] !== undefined) { values[i] = valueEncoding.decode(values[i]) } } } catch (err) { return callback(new ModuleError(`Could not decode one or more of ${values.length} value(s)`, { code: 'LEVEL_DECODE_ERROR', cause: err })) } callback(null, values) }) return callback[kPromise] } _getMany (keys, options, callback) { this.nextTick(callback, null, new Array(keys.length).fill(undefined)) } put (key, value, options, callback) { callback = getCallback(options, callback) callback = fromCallback(callback, kPromise) options = getOptions(options, this[kDefaultOptions].entry) if (this[kStatus] === 'opening') { this.defer(() => this.put(key, value, options, callback)) return callback[kPromise] } if (maybeError(this, callback)) { return callback[kPromise] } const err = this._checkKey(key) || this._checkValue(value) if (err) { this.nextTick(callback, err) return callback[kPromise] } const keyEncoding = this.keyEncoding(options.keyEncoding) const valueEncoding = this.valueEncoding(options.valueEncoding) const keyFormat = keyEncoding.format const valueFormat = valueEncoding.format // Forward encoding options if (options.keyEncoding !== keyFormat || options.valueEncoding !== valueFormat) { options = Object.assign({}, options, { keyEncoding: keyFormat, valueEncoding: valueFormat }) } const mappedKey = this.prefixKey(keyEncoding.encode(key), keyFormat) const mappedValue = valueEncoding.encode(value) this._put(mappedKey, mappedValue, options, (err) => { if (err) return callback(err) this.emit('put', key, value) callback() }) return callback[kPromise] } _put (key, value, options, callback) { this.nextTick(callback) } del (key, options, callback) { callback = getCallback(options, callback) callback = fromCallback(callback, kPromise) options = getOptions(options, this[kDefaultOptions].key) if (this[kStatus] === 'opening') { this.defer(() => this.del(key, options, callback)) return callback[kPromise] } if (maybeError(this, callback)) { return callback[kPromise] } const err = this._checkKey(key) if (err) { this.nextTick(callback, err) return callback[kPromise] } const keyEncoding = this.keyEncoding(options.keyEncoding) const keyFormat = keyEncoding.format // Forward encoding options if (options.keyEncoding !== keyFormat) { options = Object.assign({}, options, { keyEncoding: keyFormat }) } this._del(this.prefixKey(keyEncoding.encode(key), keyFormat), options, (err) => { if (err) return callback(err) this.emit('del', key) callback() }) return callback[kPromise] } _del (key, options, callback) { this.nextTick(callback) } batch (operations, options, callback) { if (!arguments.length) { if (this[kStatus] === 'opening') return new DefaultChainedBatch(this) if (this[kStatus] !== 'open') { throw new ModuleError('Database is not open', { code: 'LEVEL_DATABASE_NOT_OPEN' }) } return this._chainedBatch() } if (typeof operations === 'function') callback = operations else callback = getCallback(options, callback) callback = fromCallback(callback, kPromise) options = getOptions(options, this[kDefaultOptions].empty) if (this[kStatus] === 'opening') { this.defer(() => this.batch(operations, options, callback)) return callback[kPromise] } if (maybeError(this, callback)) { return callback[kPromise] } if (!Array.isArray(operations)) { this.nextTick(callback, new TypeError("The first argument 'operations' must be an array")) return callback[kPromise] } if (operations.length === 0) { this.nextTick(callback) return callback[kPromise] } const mapped = new Array(operations.length) const { keyEncoding: ke, valueEncoding: ve, ...forward } = options for (let i = 0; i < operations.length; i++) { if (typeof operations[i] !== 'object' || operations[i] === null) { this.nextTick(callback, new TypeError('A batch operation must be an object')) return callback[kPromise] } const op = Object.assign({}, operations[i]) if (op.type !== 'put' && op.type !== 'del') { this.nextTick(callback, new TypeError("A batch operation must have a type property that is 'put' or 'del'")) return callback[kPromise] } const err = this._checkKey(op.key) if (err) { this.nextTick(callback, err) return callback[kPromise] } const db = op.sublevel != null ? op.sublevel : this const keyEncoding = db.keyEncoding(op.keyEncoding || ke) const keyFormat = keyEncoding.format op.key = db.prefixKey(keyEncoding.encode(op.key), keyFormat) op.keyEncoding = keyFormat if (op.type === 'put') { const valueErr = this._checkValue(op.value) if (valueErr) { this.nextTick(callback, valueErr) return callback[kPromise] } const valueEncoding = db.valueEncoding(op.valueEncoding || ve) op.value = valueEncoding.encode(op.value) op.valueEncoding = valueEncoding.format } // Prevent double prefixing if (db !== this) { op.sublevel = null } mapped[i] = op } this._batch(mapped, forward, (err) => { if (err) return callback(err) this.emit('batch', operations) callback() }) return callback[kPromise] } _batch (operations, options, callback) { this.nextTick(callback) } sublevel (name, options) { return this._sublevel(name, AbstractSublevel.defaults(options)) } _sublevel (name, options) { return new AbstractSublevel(this, name, options) } prefixKey (key, keyFormat) { return key } clear (options, callback) { callback = getCallback(options, callback) callback = fromCallback(callback, kPromise) options = getOptions(options, this[kDefaultOptions].empty) if (this[kStatus] === 'opening') { this.defer(() => this.clear(options, callback)) return callback[kPromise] } if (maybeError(this, callback)) { return callback[kPromise] } const original = options const keyEncoding = this.keyEncoding(options.keyEncoding) options = rangeOptions(options, keyEncoding) options.keyEncoding = keyEncoding.format if (options.limit === 0) { this.nextTick(callback) } else { this._clear(options, (err) => { if (err) return callback(err) this.emit('clear', original) callback() }) } return callback[kPromise] } _clear (options, callback) { this.nextTick(callback) } iterator (options) { const keyEncoding = this.keyEncoding(options && options.keyEncoding) const valueEncoding = this.valueEncoding(options && options.valueEncoding) options = rangeOptions(options, keyEncoding) options.keys = options.keys !== false options.values = options.values !== false // We need the original encoding options in AbstractIterator in order to decode data options[AbstractIterator.keyEncoding] = keyEncoding options[AbstractIterator.valueEncoding] = valueEncoding // Forward encoding options to private API options.keyEncoding = keyEncoding.format options.valueEncoding = valueEncoding.format if (this[kStatus] === 'opening') { return new DeferredIterator(this, options) } else if (this[kStatus] !== 'open') { throw new ModuleError('Database is not open', { code: 'LEVEL_DATABASE_NOT_OPEN' }) } return this._iterator(options) } _iterator (options) { return new AbstractIterator(this, options) } keys (options) { // Also include valueEncoding (though unused) because we may fallback to _iterator() const keyEncoding = this.keyEncoding(options && options.keyEncoding) const valueEncoding = this.valueEncoding(options && options.valueEncoding) options = rangeOptions(options, keyEncoding) // We need the original encoding options in AbstractKeyIterator in order to decode data options[AbstractIterator.keyEncoding] = keyEncoding options[AbstractIterator.valueEncoding] = valueEncoding // Forward encoding options to private API options.keyEncoding = keyEncoding.format options.valueEncoding = valueEncoding.format if (this[kStatus] === 'opening') { return new DeferredKeyIterator(this, options) } else if (this[kStatus] !== 'open') { throw new ModuleError('Database is not open', { code: 'LEVEL_DATABASE_NOT_OPEN' }) } return this._keys(options) } _keys (options) { return new DefaultKeyIterator(this, options) } values (options) { const keyEncoding = this.keyEncoding(options && options.keyEncoding) const valueEncoding = this.valueEncoding(options && options.valueEncoding) options = rangeOptions(options, keyEncoding) // We need the original encoding options in AbstractValueIterator in order to decode data options[AbstractIterator.keyEncoding] = keyEncoding options[AbstractIterator.valueEncoding] = valueEncoding // Forward encoding options to private API options.keyEncoding = keyEncoding.format options.valueEncoding = valueEncoding.format if (this[kStatus] === 'opening') { return new DeferredValueIterator(this, options) } else if (this[kStatus] !== 'open') { throw new ModuleError('Database is not open', { code: 'LEVEL_DATABASE_NOT_OPEN' }) } return this._values(options) } _values (options) { return new DefaultValueIterator(this, options) } defer (fn) { if (typeof fn !== 'function') { throw new TypeError('The first argument must be a function') } this[kOperations].push(fn) } [kUndefer] () { if (this[kOperations].length === 0) { return } const operations = this[kOperations] this[kOperations] = [] for (const op of operations) { op() } } // TODO: docs and types attachResource (resource) { if (typeof resource !== 'object' || resource === null || typeof resource.close !== 'function') { throw new TypeError('The first argument must be a resource object') } this[kResources].add(resource) } // TODO: docs and types detachResource (resource) { this[kResources].delete(resource) } _chainedBatch () { return new DefaultChainedBatch(this) } _checkKey (key) { if (key === null || key === undefined) { return new ModuleError('Key cannot be null or undefined', { code: 'LEVEL_INVALID_KEY' }) } } _checkValue (value) { if (value === null || value === undefined) { return new ModuleError('Value cannot be null or undefined', { code: 'LEVEL_INVALID_VALUE' }) } } } // Expose browser-compatible nextTick for dependents // TODO: after we drop node 10, also use queueMicrotask in node AbstractLevel.prototype.nextTick = require('./lib/next-tick') const { AbstractSublevel } = require('./lib/abstract-sublevel')({ AbstractLevel }) exports.AbstractLevel = AbstractLevel exports.AbstractSublevel = AbstractSublevel const maybeError = function (db, callback) { if (db[kStatus] !== 'open') { db.nextTick(callback, new ModuleError('Database is not open', { code: 'LEVEL_DATABASE_NOT_OPEN' })) return true } return false } const formats = function (db) { return Object.keys(db.supports.encodings) .filter(k => !!db.supports.encodings[k]) }