'use strict' const { fromCallback } = require('catering') const ModuleError = require('module-error') const { getOptions, getCallback } = require('./lib/common') const kPromise = Symbol('promise') const kCallback = Symbol('callback') const kWorking = Symbol('working') const kHandleOne = Symbol('handleOne') const kHandleMany = Symbol('handleMany') const kAutoClose = Symbol('autoClose') const kFinishWork = Symbol('finishWork') const kReturnMany = Symbol('returnMany') const kClosing = Symbol('closing') const kHandleClose = Symbol('handleClose') const kClosed = Symbol('closed') const kCloseCallbacks = Symbol('closeCallbacks') const kKeyEncoding = Symbol('keyEncoding') const kValueEncoding = Symbol('valueEncoding') const kAbortOnClose = Symbol('abortOnClose') const kLegacy = Symbol('legacy') const kKeys = Symbol('keys') const kValues = Symbol('values') const kLimit = Symbol('limit') const kCount = Symbol('count') const emptyOptions = Object.freeze({}) const noop = () => {} let warnedEnd = false // This class is an internal utility for common functionality between AbstractIterator, // AbstractKeyIterator and AbstractValueIterator. It's not exported. class CommonIterator { constructor (db, options, legacy) { if (typeof db !== 'object' || db === null) { const hint = db === null ? 'null' : typeof db throw new TypeError(`The first argument must be an abstract-level database, received ${hint}`) } if (typeof options !== 'object' || options === null) { throw new TypeError('The second argument must be an options object') } this[kClosed] = false this[kCloseCallbacks] = [] this[kWorking] = false this[kClosing] = false this[kAutoClose] = false this[kCallback] = null this[kHandleOne] = this[kHandleOne].bind(this) this[kHandleMany] = this[kHandleMany].bind(this) this[kHandleClose] = this[kHandleClose].bind(this) this[kKeyEncoding] = options[kKeyEncoding] this[kValueEncoding] = options[kValueEncoding] this[kLegacy] = legacy this[kLimit] = Number.isInteger(options.limit) && options.limit >= 0 ? options.limit : Infinity this[kCount] = 0 // Undocumented option to abort pending work on close(). Used by the // many-level module as a temporary solution to a blocked close(). // TODO (next major): consider making this the default behavior. Native // implementations should have their own logic to safely close iterators. this[kAbortOnClose] = !!options.abortOnClose this.db = db this.db.attachResource(this) this.nextTick = db.nextTick } get count () { return this[kCount] } get limit () { return this[kLimit] } next (callback) { let promise if (callback === undefined) { promise = new Promise((resolve, reject) => { callback = (err, key, value) => { if (err) reject(err) else if (!this[kLegacy]) resolve(key) else if (key === undefined && value === undefined) resolve() else resolve([key, value]) } }) } else if (typeof callback !== 'function') { throw new TypeError('Callback must be a function') } if (this[kClosing]) { this.nextTick(callback, new ModuleError('Iterator is not open: cannot call next() after close()', { code: 'LEVEL_ITERATOR_NOT_OPEN' })) } else if (this[kWorking]) { this.nextTick(callback, new ModuleError('Iterator is busy: cannot call next() until previous call has completed', { code: 'LEVEL_ITERATOR_BUSY' })) } else { this[kWorking] = true this[kCallback] = callback if (this[kCount] >= this[kLimit]) this.nextTick(this[kHandleOne], null) else this._next(this[kHandleOne]) } return promise } _next (callback) { this.nextTick(callback) } nextv (size, options, callback) { callback = getCallback(options, callback) callback = fromCallback(callback, kPromise) options = getOptions(options, emptyOptions) if (!Number.isInteger(size)) { this.nextTick(callback, new TypeError("The first argument 'size' must be an integer")) return callback[kPromise] } if (this[kClosing]) { this.nextTick(callback, new ModuleError('Iterator is not open: cannot call nextv() after close()', { code: 'LEVEL_ITERATOR_NOT_OPEN' })) } else if (this[kWorking]) { this.nextTick(callback, new ModuleError('Iterator is busy: cannot call nextv() until previous call has completed', { code: 'LEVEL_ITERATOR_BUSY' })) } else { if (size < 1) size = 1 if (this[kLimit] < Infinity) size = Math.min(size, this[kLimit] - this[kCount]) this[kWorking] = true this[kCallback] = callback if (size <= 0) this.nextTick(this[kHandleMany], null, []) else this._nextv(size, options, this[kHandleMany]) } return callback[kPromise] } _nextv (size, options, callback) { const acc = [] const onnext = (err, key, value) => { if (err) { return callback(err) } else if (this[kLegacy] ? key === undefined && value === undefined : key === undefined) { return callback(null, acc) } acc.push(this[kLegacy] ? [key, value] : key) if (acc.length === size) { callback(null, acc) } else { this._next(onnext) } } this._next(onnext) } all (options, callback) { callback = getCallback(options, callback) callback = fromCallback(callback, kPromise) options = getOptions(options, emptyOptions) if (this[kClosing]) { this.nextTick(callback, new ModuleError('Iterator is not open: cannot call all() after close()', { code: 'LEVEL_ITERATOR_NOT_OPEN' })) } else if (this[kWorking]) { this.nextTick(callback, new ModuleError('Iterator is busy: cannot call all() until previous call has completed', { code: 'LEVEL_ITERATOR_BUSY' })) } else { this[kWorking] = true this[kCallback] = callback this[kAutoClose] = true if (this[kCount] >= this[kLimit]) this.nextTick(this[kHandleMany], null, []) else this._all(options, this[kHandleMany]) } return callback[kPromise] } _all (options, callback) { // Must count here because we're directly calling _nextv() let count = this[kCount] const acc = [] const nextv = () => { // Not configurable, because implementations should optimize _all(). const size = this[kLimit] < Infinity ? Math.min(1e3, this[kLimit] - count) : 1e3 if (size <= 0) { this.nextTick(callback, null, acc) } else { this._nextv(size, emptyOptions, onnextv) } } const onnextv = (err, items) => { if (err) { callback(err) } else if (items.length === 0) { callback(null, acc) } else { acc.push.apply(acc, items) count += items.length nextv() } } nextv() } [kFinishWork] () { const cb = this[kCallback] // Callback will be null if work was aborted on close if (this[kAbortOnClose] && cb === null) return noop this[kWorking] = false this[kCallback] = null if (this[kClosing]) this._close(this[kHandleClose]) return cb } [kReturnMany] (cb, err, items) { if (this[kAutoClose]) { this.close(cb.bind(null, err, items)) } else { cb(err, items) } } seek (target, options) { options = getOptions(options, emptyOptions) if (this[kClosing]) { // Don't throw here, to be kind to implementations that wrap // another db and don't necessarily control when the db is closed } else if (this[kWorking]) { throw new ModuleError('Iterator is busy: cannot call seek() until next() has completed', { code: 'LEVEL_ITERATOR_BUSY' }) } else { const keyEncoding = this.db.keyEncoding(options.keyEncoding || this[kKeyEncoding]) const keyFormat = keyEncoding.format if (options.keyEncoding !== keyFormat) { options = { ...options, keyEncoding: keyFormat } } const mapped = this.db.prefixKey(keyEncoding.encode(target), keyFormat) this._seek(mapped, options) } } _seek (target, options) { throw new ModuleError('Iterator does not support seek()', { code: 'LEVEL_NOT_SUPPORTED' }) } close (callback) { callback = fromCallback(callback, kPromise) if (this[kClosed]) { this.nextTick(callback) } else if (this[kClosing]) { this[kCloseCallbacks].push(callback) } else { this[kClosing] = true this[kCloseCallbacks].push(callback) if (!this[kWorking]) { this._close(this[kHandleClose]) } else if (this[kAbortOnClose]) { // Don't wait for work to finish. Subsequently ignore the result. const cb = this[kFinishWork]() cb(new ModuleError('Aborted on iterator close()', { code: 'LEVEL_ITERATOR_NOT_OPEN' })) } } return callback[kPromise] } _close (callback) { this.nextTick(callback) } [kHandleClose] () { this[kClosed] = true this.db.detachResource(this) const callbacks = this[kCloseCallbacks] this[kCloseCallbacks] = [] for (const cb of callbacks) { cb() } } async * [Symbol.asyncIterator] () { try { let item while ((item = (await this.next())) !== undefined) { yield item } } finally { if (!this[kClosed]) await this.close() } } } // For backwards compatibility this class is not (yet) called AbstractEntryIterator. class AbstractIterator extends CommonIterator { constructor (db, options) { super(db, options, true) this[kKeys] = options.keys !== false this[kValues] = options.values !== false } [kHandleOne] (err, key, value) { const cb = this[kFinishWork]() if (err) return cb(err) try { key = this[kKeys] && key !== undefined ? this[kKeyEncoding].decode(key) : undefined value = this[kValues] && value !== undefined ? this[kValueEncoding].decode(value) : undefined } catch (err) { return cb(new IteratorDecodeError('entry', err)) } if (!(key === undefined && value === undefined)) { this[kCount]++ } cb(null, key, value) } [kHandleMany] (err, entries) { const cb = this[kFinishWork]() if (err) return this[kReturnMany](cb, err) try { for (const entry of entries) { const key = entry[0] const value = entry[1] entry[0] = this[kKeys] && key !== undefined ? this[kKeyEncoding].decode(key) : undefined entry[1] = this[kValues] && value !== undefined ? this[kValueEncoding].decode(value) : undefined } } catch (err) { return this[kReturnMany](cb, new IteratorDecodeError('entries', err)) } this[kCount] += entries.length this[kReturnMany](cb, null, entries) } end (callback) { if (!warnedEnd && typeof console !== 'undefined') { warnedEnd = true console.warn(new ModuleError( 'The iterator.end() method was renamed to close() and end() is an alias that will be removed in a future version', { code: 'LEVEL_LEGACY' } )) } return this.close(callback) } } class AbstractKeyIterator extends CommonIterator { constructor (db, options) { super(db, options, false) } [kHandleOne] (err, key) { const cb = this[kFinishWork]() if (err) return cb(err) try { key = key !== undefined ? this[kKeyEncoding].decode(key) : undefined } catch (err) { return cb(new IteratorDecodeError('key', err)) } if (key !== undefined) this[kCount]++ cb(null, key) } [kHandleMany] (err, keys) { const cb = this[kFinishWork]() if (err) return this[kReturnMany](cb, err) try { for (let i = 0; i < keys.length; i++) { const key = keys[i] keys[i] = key !== undefined ? this[kKeyEncoding].decode(key) : undefined } } catch (err) { return this[kReturnMany](cb, new IteratorDecodeError('keys', err)) } this[kCount] += keys.length this[kReturnMany](cb, null, keys) } } class AbstractValueIterator extends CommonIterator { constructor (db, options) { super(db, options, false) } [kHandleOne] (err, value) { const cb = this[kFinishWork]() if (err) return cb(err) try { value = value !== undefined ? this[kValueEncoding].decode(value) : undefined } catch (err) { return cb(new IteratorDecodeError('value', err)) } if (value !== undefined) this[kCount]++ cb(null, value) } [kHandleMany] (err, values) { const cb = this[kFinishWork]() if (err) return this[kReturnMany](cb, err) try { for (let i = 0; i < values.length; i++) { const value = values[i] values[i] = value !== undefined ? this[kValueEncoding].decode(value) : undefined } } catch (err) { return this[kReturnMany](cb, new IteratorDecodeError('values', err)) } this[kCount] += values.length this[kReturnMany](cb, null, values) } } // Internal utility, not typed or exported class IteratorDecodeError extends ModuleError { constructor (subject, cause) { super(`Iterator could not decode ${subject}`, { code: 'LEVEL_DECODE_ERROR', cause }) } } // To help migrating to abstract-level for (const k of ['_ended property', '_nexting property', '_end method']) { Object.defineProperty(AbstractIterator.prototype, k.split(' ')[0], { get () { throw new ModuleError(`The ${k} has been removed`, { code: 'LEVEL_LEGACY' }) }, set () { throw new ModuleError(`The ${k} has been removed`, { code: 'LEVEL_LEGACY' }) } }) } // Exposed so that AbstractLevel can set these options AbstractIterator.keyEncoding = kKeyEncoding AbstractIterator.valueEncoding = kValueEncoding exports.AbstractIterator = AbstractIterator exports.AbstractKeyIterator = AbstractKeyIterator exports.AbstractValueIterator = AbstractValueIterator