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)
 | |
| }
 |