forked from public/fvtt-cthulhu-eternal
259 lines
7.7 KiB
JavaScript
259 lines
7.7 KiB
JavaScript
|
'use strict'
|
||
|
|
||
|
const ModuleError = require('module-error')
|
||
|
const { Buffer } = require('buffer') || {}
|
||
|
const {
|
||
|
AbstractSublevelIterator,
|
||
|
AbstractSublevelKeyIterator,
|
||
|
AbstractSublevelValueIterator
|
||
|
} = require('./abstract-sublevel-iterator')
|
||
|
|
||
|
const kPrefix = Symbol('prefix')
|
||
|
const kUpperBound = Symbol('upperBound')
|
||
|
const kPrefixRange = Symbol('prefixRange')
|
||
|
const kParent = Symbol('parent')
|
||
|
const kUnfix = Symbol('unfix')
|
||
|
|
||
|
const textEncoder = new TextEncoder()
|
||
|
const defaults = { separator: '!' }
|
||
|
|
||
|
// Wrapped to avoid circular dependency
|
||
|
module.exports = function ({ AbstractLevel }) {
|
||
|
class AbstractSublevel extends AbstractLevel {
|
||
|
static defaults (options) {
|
||
|
// To help migrating from subleveldown to abstract-level
|
||
|
if (typeof options === 'string') {
|
||
|
throw new ModuleError('The subleveldown string shorthand for { separator } has been removed', {
|
||
|
code: 'LEVEL_LEGACY'
|
||
|
})
|
||
|
} else if (options && options.open) {
|
||
|
throw new ModuleError('The subleveldown open option has been removed', {
|
||
|
code: 'LEVEL_LEGACY'
|
||
|
})
|
||
|
}
|
||
|
|
||
|
if (options == null) {
|
||
|
return defaults
|
||
|
} else if (!options.separator) {
|
||
|
return { ...options, separator: '!' }
|
||
|
} else {
|
||
|
return options
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// TODO: add autoClose option, which if true, does parent.attachResource(this)
|
||
|
constructor (db, name, options) {
|
||
|
// Don't forward AbstractSublevel options to AbstractLevel
|
||
|
const { separator, manifest, ...forward } = AbstractSublevel.defaults(options)
|
||
|
name = trim(name, separator)
|
||
|
|
||
|
// Reserve one character between separator and name to give us an upper bound
|
||
|
const reserved = separator.charCodeAt(0) + 1
|
||
|
const parent = db[kParent] || db
|
||
|
|
||
|
// Keys should sort like ['!a!', '!a!!a!', '!a"', '!aa!', '!b!'].
|
||
|
// Use ASCII for consistent length between string, Buffer and Uint8Array
|
||
|
if (!textEncoder.encode(name).every(x => x > reserved && x < 127)) {
|
||
|
throw new ModuleError(`Prefix must use bytes > ${reserved} < ${127}`, {
|
||
|
code: 'LEVEL_INVALID_PREFIX'
|
||
|
})
|
||
|
}
|
||
|
|
||
|
super(mergeManifests(parent, manifest), forward)
|
||
|
|
||
|
const prefix = (db.prefix || '') + separator + name + separator
|
||
|
const upperBound = prefix.slice(0, -1) + String.fromCharCode(reserved)
|
||
|
|
||
|
this[kParent] = parent
|
||
|
this[kPrefix] = new MultiFormat(prefix)
|
||
|
this[kUpperBound] = new MultiFormat(upperBound)
|
||
|
this[kUnfix] = new Unfixer()
|
||
|
|
||
|
this.nextTick = parent.nextTick
|
||
|
}
|
||
|
|
||
|
prefixKey (key, keyFormat) {
|
||
|
if (keyFormat === 'utf8') {
|
||
|
return this[kPrefix].utf8 + key
|
||
|
} else if (key.byteLength === 0) {
|
||
|
// Fast path for empty key (no copy)
|
||
|
return this[kPrefix][keyFormat]
|
||
|
} else if (keyFormat === 'view') {
|
||
|
const view = this[kPrefix].view
|
||
|
const result = new Uint8Array(view.byteLength + key.byteLength)
|
||
|
|
||
|
result.set(view, 0)
|
||
|
result.set(key, view.byteLength)
|
||
|
|
||
|
return result
|
||
|
} else {
|
||
|
const buffer = this[kPrefix].buffer
|
||
|
return Buffer.concat([buffer, key], buffer.byteLength + key.byteLength)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Not exposed for now.
|
||
|
[kPrefixRange] (range, keyFormat) {
|
||
|
if (range.gte !== undefined) {
|
||
|
range.gte = this.prefixKey(range.gte, keyFormat)
|
||
|
} else if (range.gt !== undefined) {
|
||
|
range.gt = this.prefixKey(range.gt, keyFormat)
|
||
|
} else {
|
||
|
range.gte = this[kPrefix][keyFormat]
|
||
|
}
|
||
|
|
||
|
if (range.lte !== undefined) {
|
||
|
range.lte = this.prefixKey(range.lte, keyFormat)
|
||
|
} else if (range.lt !== undefined) {
|
||
|
range.lt = this.prefixKey(range.lt, keyFormat)
|
||
|
} else {
|
||
|
range.lte = this[kUpperBound][keyFormat]
|
||
|
}
|
||
|
}
|
||
|
|
||
|
get prefix () {
|
||
|
return this[kPrefix].utf8
|
||
|
}
|
||
|
|
||
|
get db () {
|
||
|
return this[kParent]
|
||
|
}
|
||
|
|
||
|
_open (options, callback) {
|
||
|
// The parent db must open itself or be (re)opened by the user because
|
||
|
// a sublevel should not initiate state changes on the rest of the db.
|
||
|
this[kParent].open({ passive: true }, callback)
|
||
|
}
|
||
|
|
||
|
_put (key, value, options, callback) {
|
||
|
this[kParent].put(key, value, options, callback)
|
||
|
}
|
||
|
|
||
|
_get (key, options, callback) {
|
||
|
this[kParent].get(key, options, callback)
|
||
|
}
|
||
|
|
||
|
_getMany (keys, options, callback) {
|
||
|
this[kParent].getMany(keys, options, callback)
|
||
|
}
|
||
|
|
||
|
_del (key, options, callback) {
|
||
|
this[kParent].del(key, options, callback)
|
||
|
}
|
||
|
|
||
|
_batch (operations, options, callback) {
|
||
|
this[kParent].batch(operations, options, callback)
|
||
|
}
|
||
|
|
||
|
_clear (options, callback) {
|
||
|
// TODO (refactor): move to AbstractLevel
|
||
|
this[kPrefixRange](options, options.keyEncoding)
|
||
|
this[kParent].clear(options, callback)
|
||
|
}
|
||
|
|
||
|
_iterator (options) {
|
||
|
// TODO (refactor): move to AbstractLevel
|
||
|
this[kPrefixRange](options, options.keyEncoding)
|
||
|
const iterator = this[kParent].iterator(options)
|
||
|
const unfix = this[kUnfix].get(this[kPrefix].utf8.length, options.keyEncoding)
|
||
|
return new AbstractSublevelIterator(this, options, iterator, unfix)
|
||
|
}
|
||
|
|
||
|
_keys (options) {
|
||
|
this[kPrefixRange](options, options.keyEncoding)
|
||
|
const iterator = this[kParent].keys(options)
|
||
|
const unfix = this[kUnfix].get(this[kPrefix].utf8.length, options.keyEncoding)
|
||
|
return new AbstractSublevelKeyIterator(this, options, iterator, unfix)
|
||
|
}
|
||
|
|
||
|
_values (options) {
|
||
|
this[kPrefixRange](options, options.keyEncoding)
|
||
|
const iterator = this[kParent].values(options)
|
||
|
return new AbstractSublevelValueIterator(this, options, iterator)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return { AbstractSublevel }
|
||
|
}
|
||
|
|
||
|
const mergeManifests = function (parent, manifest) {
|
||
|
return {
|
||
|
// Inherit manifest of parent db
|
||
|
...parent.supports,
|
||
|
|
||
|
// Disable unsupported features
|
||
|
createIfMissing: false,
|
||
|
errorIfExists: false,
|
||
|
|
||
|
// Unset additional events because we're not forwarding them
|
||
|
events: {},
|
||
|
|
||
|
// Unset additional methods (like approximateSize) which we can't support here unless
|
||
|
// the AbstractSublevel class is overridden by an implementation of `abstract-level`.
|
||
|
additionalMethods: {},
|
||
|
|
||
|
// Inherit manifest of custom AbstractSublevel subclass. Such a class is not
|
||
|
// allowed to override encodings.
|
||
|
...manifest,
|
||
|
|
||
|
encodings: {
|
||
|
utf8: supportsEncoding(parent, 'utf8'),
|
||
|
buffer: supportsEncoding(parent, 'buffer'),
|
||
|
view: supportsEncoding(parent, 'view')
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
const supportsEncoding = function (parent, encoding) {
|
||
|
// Prefer a non-transcoded encoding for optimal performance
|
||
|
return parent.supports.encodings[encoding]
|
||
|
? parent.keyEncoding(encoding).name === encoding
|
||
|
: false
|
||
|
}
|
||
|
|
||
|
class MultiFormat {
|
||
|
constructor (key) {
|
||
|
this.utf8 = key
|
||
|
this.view = textEncoder.encode(key)
|
||
|
this.buffer = Buffer ? Buffer.from(this.view.buffer, 0, this.view.byteLength) : {}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class Unfixer {
|
||
|
constructor () {
|
||
|
this.cache = new Map()
|
||
|
}
|
||
|
|
||
|
get (prefixLength, keyFormat) {
|
||
|
let unfix = this.cache.get(keyFormat)
|
||
|
|
||
|
if (unfix === undefined) {
|
||
|
if (keyFormat === 'view') {
|
||
|
unfix = function (prefixLength, key) {
|
||
|
// Avoid Uint8Array#slice() because it copies
|
||
|
return key.subarray(prefixLength)
|
||
|
}.bind(null, prefixLength)
|
||
|
} else {
|
||
|
unfix = function (prefixLength, key) {
|
||
|
// Avoid Buffer#subarray() because it's slow
|
||
|
return key.slice(prefixLength)
|
||
|
}.bind(null, prefixLength)
|
||
|
}
|
||
|
|
||
|
this.cache.set(keyFormat, unfix)
|
||
|
}
|
||
|
|
||
|
return unfix
|
||
|
}
|
||
|
}
|
||
|
|
||
|
const trim = function (str, char) {
|
||
|
let start = 0
|
||
|
let end = str.length
|
||
|
|
||
|
while (start < end && str[start] === char) start++
|
||
|
while (end > start && str[end - 1] === char) end--
|
||
|
|
||
|
return str.slice(start, end)
|
||
|
}
|