819 lines
23 KiB
JavaScript
Raw Normal View History

'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])
}