forked from public/fvtt-cthulhu-eternal
281 lines
7.6 KiB
JavaScript
281 lines
7.6 KiB
JavaScript
'use strict'
|
|
|
|
const ModuleError = require('module-error')
|
|
const { AbstractLevel, AbstractChainedBatch } = require('..')
|
|
const { AbstractIterator, AbstractKeyIterator, AbstractValueIterator } = require('..')
|
|
|
|
const spies = []
|
|
|
|
exports.verifyNotFoundError = function (err) {
|
|
return err.code === 'LEVEL_NOT_FOUND' && err.notFound === true && err.status === 404
|
|
}
|
|
|
|
exports.illegalKeys = [
|
|
{ name: 'null key', key: null },
|
|
{ name: 'undefined key', key: undefined }
|
|
]
|
|
|
|
exports.illegalValues = [
|
|
{ name: 'null key', value: null },
|
|
{ name: 'undefined value', value: undefined }
|
|
]
|
|
|
|
/**
|
|
* Wrap a callback to check that it's called asynchronously. Must be
|
|
* combined with a `ctx()`, `with()` or `end()` call.
|
|
*
|
|
* @param {function} cb Callback to check.
|
|
* @param {string} name Optional callback name to use in assertion messages.
|
|
* @returns {function} Wrapped callback.
|
|
*/
|
|
exports.assertAsync = function (cb, name) {
|
|
const spy = {
|
|
called: false,
|
|
name: name || cb.name || 'anonymous'
|
|
}
|
|
|
|
spies.push(spy)
|
|
|
|
return function (...args) {
|
|
spy.called = true
|
|
return cb.apply(this, args)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Verify that callbacks wrapped with `assertAsync()` were not yet called.
|
|
* @param {import('tape').Test} t Tape test object.
|
|
*/
|
|
exports.assertAsync.end = function (t) {
|
|
for (const { called, name } of spies.splice(0, spies.length)) {
|
|
t.is(called, false, `callback (${name}) is asynchronous`)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Wrap a test function to verify `assertAsync()` spies at the end.
|
|
* @param {import('tape').TestCase} test Test function to be passed to `tape()`.
|
|
* @returns {import('tape').TestCase} Wrapped test function.
|
|
*/
|
|
exports.assertAsync.ctx = function (test) {
|
|
return function (...args) {
|
|
const ret = test.call(this, ...args)
|
|
exports.assertAsync.end(args[0])
|
|
return ret
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Wrap an arbitrary callback to verify `assertAsync()` spies at the end.
|
|
* @param {import('tape').Test} t Tape test object.
|
|
* @param {function} cb Callback to wrap.
|
|
* @returns {function} Wrapped callback.
|
|
*/
|
|
exports.assertAsync.with = function (t, cb) {
|
|
return function (...args) {
|
|
const ret = cb.call(this, ...args)
|
|
exports.assertAsync.end(t)
|
|
return ret
|
|
}
|
|
}
|
|
|
|
exports.mockLevel = function (methods, ...args) {
|
|
class TestLevel extends AbstractLevel {}
|
|
for (const k in methods) TestLevel.prototype[k] = methods[k]
|
|
if (!args.length) args = [{ encodings: { utf8: true } }]
|
|
return new TestLevel(...args)
|
|
}
|
|
|
|
exports.mockIterator = function (db, options, methods, ...args) {
|
|
class TestIterator extends AbstractIterator {}
|
|
for (const k in methods) TestIterator.prototype[k] = methods[k]
|
|
return new TestIterator(db, options, ...args)
|
|
}
|
|
|
|
exports.mockChainedBatch = function (db, methods, ...args) {
|
|
class TestBatch extends AbstractChainedBatch {}
|
|
for (const k in methods) TestBatch.prototype[k] = methods[k]
|
|
return new TestBatch(db, ...args)
|
|
}
|
|
|
|
// Mock encoding where null and undefined are significant types
|
|
exports.nullishEncoding = {
|
|
name: 'nullish',
|
|
format: 'utf8',
|
|
encode (v) {
|
|
return v === null ? '\x00' : v === undefined ? '\xff' : String(v)
|
|
},
|
|
decode (v) {
|
|
return v === '\x00' ? null : v === '\xff' ? undefined : v
|
|
}
|
|
}
|
|
|
|
const kEntries = Symbol('entries')
|
|
const kPosition = Symbol('position')
|
|
const kOptions = Symbol('options')
|
|
|
|
/**
|
|
* A minimal and non-optimized implementation for use in tests. Only supports utf8.
|
|
* Don't use this as a reference implementation.
|
|
*/
|
|
class MinimalLevel extends AbstractLevel {
|
|
constructor (options) {
|
|
super({ encodings: { utf8: true }, seek: true }, options)
|
|
this[kEntries] = new Map()
|
|
}
|
|
|
|
_put (key, value, options, callback) {
|
|
this[kEntries].set(key, value)
|
|
this.nextTick(callback)
|
|
}
|
|
|
|
_get (key, options, callback) {
|
|
const value = this[kEntries].get(key)
|
|
|
|
if (value === undefined) {
|
|
return this.nextTick(callback, new ModuleError(`Key ${key} was not found`, {
|
|
code: 'LEVEL_NOT_FOUND'
|
|
}))
|
|
}
|
|
|
|
this.nextTick(callback, null, value)
|
|
}
|
|
|
|
_getMany (keys, options, callback) {
|
|
const values = keys.map(k => this[kEntries].get(k))
|
|
this.nextTick(callback, null, values)
|
|
}
|
|
|
|
_del (key, options, callback) {
|
|
this[kEntries].delete(key)
|
|
this.nextTick(callback)
|
|
}
|
|
|
|
_clear (options, callback) {
|
|
for (const [k] of sliceEntries(this[kEntries], options, true)) {
|
|
this[kEntries].delete(k)
|
|
}
|
|
|
|
this.nextTick(callback)
|
|
}
|
|
|
|
_batch (operations, options, callback) {
|
|
const entries = new Map(this[kEntries])
|
|
|
|
for (const op of operations) {
|
|
if (op.type === 'put') entries.set(op.key, op.value)
|
|
else entries.delete(op.key)
|
|
}
|
|
|
|
this[kEntries] = entries
|
|
this.nextTick(callback)
|
|
}
|
|
|
|
_iterator (options) {
|
|
return new MinimalIterator(this, options)
|
|
}
|
|
|
|
_keys (options) {
|
|
return new MinimalKeyIterator(this, options)
|
|
}
|
|
|
|
_values (options) {
|
|
return new MinimalValueIterator(this, options)
|
|
}
|
|
}
|
|
|
|
class MinimalIterator extends AbstractIterator {
|
|
constructor (db, options) {
|
|
super(db, options)
|
|
this[kEntries] = sliceEntries(db[kEntries], options, false)
|
|
this[kOptions] = options
|
|
this[kPosition] = 0
|
|
}
|
|
}
|
|
|
|
class MinimalKeyIterator extends AbstractKeyIterator {
|
|
constructor (db, options) {
|
|
super(db, options)
|
|
this[kEntries] = sliceEntries(db[kEntries], options, false)
|
|
this[kOptions] = options
|
|
this[kPosition] = 0
|
|
}
|
|
}
|
|
|
|
class MinimalValueIterator extends AbstractValueIterator {
|
|
constructor (db, options) {
|
|
super(db, options)
|
|
this[kEntries] = sliceEntries(db[kEntries], options, false)
|
|
this[kOptions] = options
|
|
this[kPosition] = 0
|
|
}
|
|
}
|
|
|
|
for (const Ctor of [MinimalIterator, MinimalKeyIterator, MinimalValueIterator]) {
|
|
const mapEntry = Ctor === MinimalIterator ? e => e : Ctor === MinimalKeyIterator ? e => e[0] : e => e[1]
|
|
|
|
Ctor.prototype._next = function (callback) {
|
|
const entry = this[kEntries][this[kPosition]++]
|
|
if (entry === undefined) return this.nextTick(callback)
|
|
if (Ctor === MinimalIterator) this.nextTick(callback, null, entry[0], entry[1])
|
|
else this.nextTick(callback, null, mapEntry(entry))
|
|
}
|
|
|
|
Ctor.prototype._nextv = function (size, options, callback) {
|
|
const entries = this[kEntries].slice(this[kPosition], this[kPosition] + size)
|
|
this[kPosition] += entries.length
|
|
this.nextTick(callback, null, entries.map(mapEntry))
|
|
}
|
|
|
|
Ctor.prototype._all = function (options, callback) {
|
|
const end = this.limit - this.count + this[kPosition]
|
|
const entries = this[kEntries].slice(this[kPosition], end)
|
|
this[kPosition] = this[kEntries].length
|
|
this.nextTick(callback, null, entries.map(mapEntry))
|
|
}
|
|
|
|
Ctor.prototype._seek = function (target, options) {
|
|
this[kPosition] = this[kEntries].length
|
|
|
|
if (!outOfRange(target, this[kOptions])) {
|
|
// Don't care about performance here
|
|
for (let i = 0; i < this[kPosition]; i++) {
|
|
const key = this[kEntries][i][0]
|
|
|
|
if (this[kOptions].reverse ? key <= target : key >= target) {
|
|
this[kPosition] = i
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const outOfRange = function (target, options) {
|
|
if ('gte' in options) {
|
|
if (target < options.gte) return true
|
|
} else if ('gt' in options) {
|
|
if (target <= options.gt) return true
|
|
}
|
|
|
|
if ('lte' in options) {
|
|
if (target > options.lte) return true
|
|
} else if ('lt' in options) {
|
|
if (target >= options.lt) return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
const sliceEntries = function (entries, options, applyLimit) {
|
|
entries = Array.from(entries)
|
|
.filter((e) => !outOfRange(e[0], options))
|
|
.sort((a, b) => a[0] > b[0] ? 1 : a[0] < b[0] ? -1 : 0)
|
|
|
|
if (options.reverse) entries.reverse()
|
|
if (applyLimit && options.limit !== -1) entries = entries.slice(0, options.limit)
|
|
|
|
return entries
|
|
}
|
|
|
|
exports.MinimalLevel = MinimalLevel
|