491 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			491 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| '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
 |