forked from public/fvtt-cthulhu-eternal
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
|