diff --git a/.airtap.yml b/.airtap.yml index 5a58d52..05d1af6 100644 --- a/.airtap.yml +++ b/.airtap.yml @@ -12,3 +12,8 @@ presets: - airtap-electron browsers: - name: electron + +# Until airtap switches to rollup +browserify: + - transform: babelify + presets: ["@babel/preset-env"] diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 96ce5ea..e0fb910 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -7,10 +7,12 @@ updates: ignore: - dependency-name: standard - dependency-name: ts-standard - - dependency-name: '@types/node' + - dependency-name: "@types/node" - dependency-name: voxpelli/tsconfig - dependency-name: typescript - dependency-name: hallmark + - dependency-name: "@babel/preset-env" + - dependency-name: babelify # Stay on the 3rd or 4th oldest stable release, per # https://www.electronjs.org/docs/latest/tutorial/electron-timelines#version-support-policy diff --git a/abstract-chained-batch.js b/abstract-chained-batch.js index eb2df4e..459d637 100644 --- a/abstract-chained-batch.js +++ b/abstract-chained-batch.js @@ -6,18 +6,20 @@ const { getOptions, emptyOptions, noop } = require('./lib/common') const { prefixDescendantKey, isDescendant } = require('./lib/prefixes') const { PrewriteBatch } = require('./lib/prewrite-batch') -const kStatus = Symbol('status') const kPublicOperations = Symbol('publicOperations') -const kLegacyOperations = Symbol('legacyOperations') const kPrivateOperations = Symbol('privateOperations') -const kClosePromise = Symbol('closePromise') -const kLength = Symbol('length') -const kPrewriteRun = Symbol('prewriteRun') -const kPrewriteBatch = Symbol('prewriteBatch') -const kPrewriteData = Symbol('prewriteData') -const kAddMode = Symbol('addMode') class AbstractChainedBatch { + #status = 'open' + #length = 0 + #closePromise = null + #publicOperations + #legacyOperations + #prewriteRun + #prewriteBatch + #prewriteData + #addMode + constructor (db, options) { if (typeof db !== 'object' || db === null) { const hint = db === null ? 'null' : typeof db @@ -29,17 +31,14 @@ class AbstractChainedBatch { // Operations for write event. We can skip populating this array (and cloning of // operations, which is the expensive part) if there are 0 write event listeners. - this[kPublicOperations] = enableWriteEvent ? [] : null + this.#publicOperations = enableWriteEvent ? [] : null // Operations for legacy batch event. If user opted-in to write event or prewrite // hook, skip legacy batch event. We can't skip the batch event based on listener // count, because a listener may be added between put() or del() and write(). - this[kLegacyOperations] = enableWriteEvent || enablePrewriteHook ? null : [] + this.#legacyOperations = enableWriteEvent || enablePrewriteHook ? null : [] - this[kLength] = 0 - this[kStatus] = 'open' - this[kClosePromise] = null - this[kAddMode] = getOptions(options, emptyOptions).add === true + this.#addMode = getOptions(options, emptyOptions).add === true if (enablePrewriteHook) { // Use separate arrays to collect operations added by hook functions, because @@ -47,13 +46,13 @@ class AbstractChainedBatch { // exists to separate internal data from the public PrewriteBatch interface. const data = new PrewriteData([], enableWriteEvent ? [] : null) - this[kPrewriteData] = data - this[kPrewriteBatch] = new PrewriteBatch(db, data[kPrivateOperations], data[kPublicOperations]) - this[kPrewriteRun] = db.hooks.prewrite.run // TODO: document why + this.#prewriteData = data + this.#prewriteBatch = new PrewriteBatch(db, data[kPrivateOperations], data[kPublicOperations]) + this.#prewriteRun = db.hooks.prewrite.run // TODO: document why } else { - this[kPrewriteData] = null - this[kPrewriteBatch] = null - this[kPrewriteRun] = null + this.#prewriteData = null + this.#prewriteBatch = null + this.#prewriteRun = null } this.db = db @@ -61,15 +60,15 @@ class AbstractChainedBatch { } get length () { - if (this[kPrewriteData] !== null) { - return this[kLength] + this[kPrewriteData].length + if (this.#prewriteData !== null) { + return this.#length + this.#prewriteData.length } else { - return this[kLength] + return this.#length } } put (key, value, options) { - assertStatus(this) + this.#assertStatus() options = getOptions(options, emptyOptions) const delegated = options.sublevel != null @@ -90,7 +89,7 @@ class AbstractChainedBatch { valueEncoding: db.valueEncoding(options.valueEncoding) }) - if (this[kPrewriteRun] !== null) { + if (this.#prewriteRun !== null) { try { // Note: we could have chosen to recurse here so that prewriteBatch.put() would // call this.put(). But then operations added by hook functions would be inserted @@ -99,7 +98,7 @@ class AbstractChainedBatch { // chained batch though, which is that it avoids blocking the event loop with // more than one operation at a time. On the other hand, if operations added by // hook functions are adjacent (i.e. sorted) committing them should be faster. - this[kPrewriteRun](op, this[kPrewriteBatch]) + this.#prewriteRun(op, this.#prewriteBatch) // Normalize encodings again in case they were modified op.keyEncoding = db.keyEncoding(op.keyEncoding) @@ -134,7 +133,7 @@ class AbstractChainedBatch { } // If the sublevel is not a descendant then we shouldn't emit events - if (this[kPublicOperations] !== null && !siblings) { + if (this.#publicOperations !== null && !siblings) { // Clone op before we mutate it for the private API const publicOperation = Object.assign({}, op) publicOperation.encodedKey = encodedKey @@ -148,15 +147,15 @@ class AbstractChainedBatch { publicOperation.valueEncoding = this.db.valueEncoding(valueFormat) } - this[kPublicOperations].push(publicOperation) - } else if (this[kLegacyOperations] !== null && !siblings) { + this.#publicOperations.push(publicOperation) + } else if (this.#legacyOperations !== null && !siblings) { const legacyOperation = Object.assign({}, original) legacyOperation.type = 'put' legacyOperation.key = key legacyOperation.value = value - this[kLegacyOperations].push(legacyOperation) + this.#legacyOperations.push(legacyOperation) } // If we're forwarding the sublevel option then don't prefix the key yet @@ -165,7 +164,7 @@ class AbstractChainedBatch { op.keyEncoding = keyFormat op.valueEncoding = valueFormat - if (this[kAddMode]) { + if (this.#addMode) { this._add(op) } else { // This "operation as options" trick avoids further cloning @@ -173,14 +172,14 @@ class AbstractChainedBatch { } // Increment only on success - this[kLength]++ + this.#length++ return this } _put (key, value, options) {} del (key, options) { - assertStatus(this) + this.#assertStatus() options = getOptions(options, emptyOptions) const delegated = options.sublevel != null @@ -197,9 +196,9 @@ class AbstractChainedBatch { keyEncoding: db.keyEncoding(options.keyEncoding) }) - if (this[kPrewriteRun] !== null) { + if (this.#prewriteRun !== null) { try { - this[kPrewriteRun](op, this[kPrewriteBatch]) + this.#prewriteRun(op, this.#prewriteBatch) // Normalize encoding again in case it was modified op.keyEncoding = db.keyEncoding(op.keyEncoding) @@ -220,7 +219,7 @@ class AbstractChainedBatch { // Prevent double prefixing if (delegated) op.sublevel = null - if (this[kPublicOperations] !== null) { + if (this.#publicOperations !== null) { // Clone op before we mutate it for the private API const publicOperation = Object.assign({}, op) publicOperation.encodedKey = encodedKey @@ -231,20 +230,20 @@ class AbstractChainedBatch { publicOperation.keyEncoding = this.db.keyEncoding(keyFormat) } - this[kPublicOperations].push(publicOperation) - } else if (this[kLegacyOperations] !== null) { + this.#publicOperations.push(publicOperation) + } else if (this.#legacyOperations !== null) { const legacyOperation = Object.assign({}, original) legacyOperation.type = 'del' legacyOperation.key = key - this[kLegacyOperations].push(legacyOperation) + this.#legacyOperations.push(legacyOperation) } op.key = this.db.prefixKey(encodedKey, keyFormat, true) op.keyEncoding = keyFormat - if (this[kAddMode]) { + if (this.#addMode) { this._add(op) } else { // This "operation as options" trick avoids further cloning @@ -252,7 +251,7 @@ class AbstractChainedBatch { } // Increment only on success - this[kLength]++ + this.#length++ return this } @@ -261,37 +260,37 @@ class AbstractChainedBatch { _add (op) {} clear () { - assertStatus(this) + this.#assertStatus() this._clear() - if (this[kPublicOperations] !== null) this[kPublicOperations] = [] - if (this[kLegacyOperations] !== null) this[kLegacyOperations] = [] - if (this[kPrewriteData] !== null) this[kPrewriteData].clear() + if (this.#publicOperations !== null) this.#publicOperations = [] + if (this.#legacyOperations !== null) this.#legacyOperations = [] + if (this.#prewriteData !== null) this.#prewriteData.clear() - this[kLength] = 0 + this.#length = 0 return this } _clear () {} async write (options) { - assertStatus(this) + this.#assertStatus() options = getOptions(options) - if (this[kLength] === 0) { + if (this.#length === 0) { return this.close() } else { - this[kStatus] = 'writing' + this.#status = 'writing' // Prepare promise in case close() is called in the mean time - const close = prepareClose(this) + const close = this.#prepareClose() try { // Process operations added by prewrite hook functions - if (this[kPrewriteData] !== null) { - const publicOperations = this[kPrewriteData][kPublicOperations] - const privateOperations = this[kPrewriteData][kPrivateOperations] - const length = this[kPrewriteData].length + if (this.#prewriteData !== null) { + const publicOperations = this.#prewriteData[kPublicOperations] + const privateOperations = this.#prewriteData[kPrivateOperations] + const length = this.#prewriteData.length for (let i = 0; i < length; i++) { const op = privateOperations[i] @@ -300,7 +299,7 @@ class AbstractChainedBatch { // status isn't exposed to the private API, so there's no difference in state // from that perspective, unless an implementation overrides the public write() // method at its own risk. - if (this[kAddMode]) { + if (this.#addMode) { this._add(op) } else if (op.type === 'put') { this._put(op.key, op.value, op) @@ -310,7 +309,7 @@ class AbstractChainedBatch { } if (publicOperations !== null && length !== 0) { - this[kPublicOperations] = this[kPublicOperations].concat(publicOperations) + this.#publicOperations = this.#publicOperations.concat(publicOperations) } } @@ -319,7 +318,7 @@ class AbstractChainedBatch { close() try { - await this[kClosePromise] + await this.#closePromise } catch (closeErr) { // eslint-disable-next-line no-ex-assign err = combineErrors([err, closeErr]) @@ -332,54 +331,73 @@ class AbstractChainedBatch { // Emit after initiating the closing, because event may trigger a // db close which in turn triggers (idempotently) closing this batch. - if (this[kPublicOperations] !== null) { - this.db.emit('write', this[kPublicOperations]) - } else if (this[kLegacyOperations] !== null) { - this.db.emit('batch', this[kLegacyOperations]) + if (this.#publicOperations !== null) { + this.db.emit('write', this.#publicOperations) + } else if (this.#legacyOperations !== null) { + this.db.emit('batch', this.#legacyOperations) } - return this[kClosePromise] + return this.#closePromise } } async _write (options) {} async close () { - if (this[kClosePromise] !== null) { + if (this.#closePromise !== null) { // First caller of close() or write() is responsible for error - return this[kClosePromise].catch(noop) + return this.#closePromise.catch(noop) } else { // Wrap promise to avoid race issues on recursive calls - prepareClose(this)() - return this[kClosePromise] + this.#prepareClose()() + return this.#closePromise } } async _close () {} -} -if (typeof Symbol.asyncDispose === 'symbol') { - AbstractChainedBatch.prototype[Symbol.asyncDispose] = async function () { - return this.close() + #assertStatus () { + if (this.#status !== 'open') { + throw new ModuleError('Batch is not open: cannot change operations after write() or close()', { + code: 'LEVEL_BATCH_NOT_OPEN' + }) + } + + // Can technically be removed, because it's no longer possible to call db.batch() when + // status is not 'open', and db.close() closes the batch. Keep for now, in case of + // unforseen userland behaviors. + if (this.db.status !== 'open') { + /* istanbul ignore next */ + throw new ModuleError('Database is not open', { + code: 'LEVEL_DATABASE_NOT_OPEN' + }) + } } -} -const prepareClose = function (batch) { - let close + #prepareClose () { + let close - batch[kClosePromise] = new Promise((resolve, reject) => { - close = () => { - privateClose(batch).then(resolve, reject) - } - }) + this.#closePromise = new Promise((resolve, reject) => { + close = () => { + this.#privateClose().then(resolve, reject) + } + }) + + return close + } - return close + async #privateClose () { + // TODO: should we not set status earlier? + this.#status = 'closing' + await this._close() + this.db.detachResource(this) + } } -const privateClose = async function (batch) { - batch[kStatus] = 'closing' - await batch._close() - batch.db.detachResource(batch) +if (typeof Symbol.asyncDispose === 'symbol') { + AbstractChainedBatch.prototype[Symbol.asyncDispose] = async function () { + return this.close() + } } class PrewriteData { @@ -405,22 +423,4 @@ class PrewriteData { } } -const assertStatus = function (batch) { - if (batch[kStatus] !== 'open') { - throw new ModuleError('Batch is not open: cannot change operations after write() or close()', { - code: 'LEVEL_BATCH_NOT_OPEN' - }) - } - - // Can technically be removed, because it's no longer possible to call db.batch() when - // status is not 'open', and db.close() closes the batch. Keep for now, in case of - // unforseen userland behaviors. - if (batch.db.status !== 'open') { - /* istanbul ignore next */ - throw new ModuleError('Database is not open', { - code: 'LEVEL_DATABASE_NOT_OPEN' - }) - } -} - exports.AbstractChainedBatch = AbstractChainedBatch diff --git a/abstract-iterator.js b/abstract-iterator.js index c2e2bcc..8506895 100644 --- a/abstract-iterator.js +++ b/abstract-iterator.js @@ -5,24 +5,23 @@ const combineErrors = require('maybe-combine-errors') const { getOptions, emptyOptions, noop } = require('./lib/common') const { AbortError } = require('./lib/errors') -const kWorking = Symbol('working') const kDecodeOne = Symbol('decodeOne') const kDecodeMany = Symbol('decodeMany') -const kSignal = Symbol('signal') -const kPendingClose = Symbol('pendingClose') -const kClosingPromise = Symbol('closingPromise') const kKeyEncoding = Symbol('keyEncoding') const kValueEncoding = Symbol('valueEncoding') -const kKeys = Symbol('keys') -const kValues = Symbol('values') -const kLimit = Symbol('limit') -const kCount = Symbol('count') -const kEnded = Symbol('ended') -const kSnapshot = Symbol('snapshot') // This class is an internal utility for common functionality between AbstractIterator, // AbstractKeyIterator and AbstractValueIterator. It's not exported. class CommonIterator { + #working = false + #pendingClose = null + #closingPromise = null + #count = 0 + #signal + #limit + #ended + #snapshot + constructor (db, options) { if (typeof db !== 'object' || db === null) { const hint = db === null ? 'null' : typeof db @@ -33,45 +32,42 @@ class CommonIterator { throw new TypeError('The second argument must be an options object') } - this[kWorking] = false - this[kPendingClose] = null - this[kClosingPromise] = null this[kKeyEncoding] = options[kKeyEncoding] this[kValueEncoding] = options[kValueEncoding] - this[kLimit] = Number.isInteger(options.limit) && options.limit >= 0 ? options.limit : Infinity - this[kCount] = 0 - this[kSignal] = options.signal != null ? options.signal : null - this[kSnapshot] = options.snapshot != null ? options.snapshot : null + + this.#limit = Number.isInteger(options.limit) && options.limit >= 0 ? options.limit : Infinity + this.#signal = options.signal != null ? options.signal : null + this.#snapshot = options.snapshot != null ? options.snapshot : null // Ending means reaching the natural end of the data and (unlike closing) that can // be reset by seek(), unless the limit was reached. - this[kEnded] = false + this.#ended = false this.db = db this.db.attachResource(this) } get count () { - return this[kCount] + return this.#count } get limit () { - return this[kLimit] + return this.#limit } async next () { - startWork(this) + this.#startWork() try { - if (this[kEnded] || this[kCount] >= this[kLimit]) { - this[kEnded] = true + if (this.#ended || this.#count >= this.#limit) { + this.#ended = true return undefined } let item = await this._next() if (item === undefined) { - this[kEnded] = true + this.#ended = true return undefined } @@ -81,10 +77,10 @@ class CommonIterator { throw new IteratorDecodeError(err) } - this[kCount]++ + this.#count++ return item } finally { - endWork(this) + this.#endWork() } } @@ -98,20 +94,20 @@ class CommonIterator { options = getOptions(options, emptyOptions) if (size < 1) size = 1 - if (this[kLimit] < Infinity) size = Math.min(size, this[kLimit] - this[kCount]) + if (this.#limit < Infinity) size = Math.min(size, this.#limit - this.#count) - startWork(this) + this.#startWork() try { - if (this[kEnded] || size <= 0) { - this[kEnded] = true + if (this.#ended || size <= 0) { + this.#ended = true return [] } const items = await this._nextv(size, options) if (items.length === 0) { - this[kEnded] = true + this.#ended = true return items } @@ -121,10 +117,10 @@ class CommonIterator { throw new IteratorDecodeError(err) } - this[kCount] += items.length + this.#count += items.length return items } finally { - endWork(this) + this.#endWork() } } @@ -138,7 +134,7 @@ class CommonIterator { acc.push(item) } else { // Must track this here because we're directly calling _next() - this[kEnded] = true + this.#ended = true break } } @@ -148,10 +144,10 @@ class CommonIterator { async all (options) { options = getOptions(options, emptyOptions) - startWork(this) + this.#startWork() try { - if (this[kEnded] || this[kCount] >= this[kLimit]) { + if (this.#ended || this.#count >= this.#limit) { return [] } @@ -163,16 +159,16 @@ class CommonIterator { throw new IteratorDecodeError(err) } - this[kCount] += items.length + this.#count += items.length return items } catch (err) { - endWork(this) - await destroy(this, err) + this.#endWork() + await this.#destroy(err) } finally { - this[kEnded] = true + this.#ended = true - if (this[kWorking]) { - endWork(this) + if (this.#working) { + this.#endWork() await this.close() } } @@ -180,13 +176,13 @@ class CommonIterator { async _all (options) { // Must count here because we're directly calling _nextv() - let count = this[kCount] + let count = this.#count const acc = [] while (true) { // Not configurable, because implementations should optimize _all(). - const size = this[kLimit] < Infinity ? Math.min(1e3, this[kLimit] - count) : 1e3 + const size = this.#limit < Infinity ? Math.min(1e3, this.#limit - count) : 1e3 if (size <= 0) { return acc @@ -206,10 +202,10 @@ class CommonIterator { seek (target, options) { options = getOptions(options, emptyOptions) - if (this[kClosingPromise] !== null) { + if (this.#closingPromise !== null) { // 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]) { + } else if (this.#working) { throw new ModuleError('Iterator is busy: cannot call seek() until next() has completed', { code: 'LEVEL_ITERATOR_BUSY' }) @@ -225,7 +221,7 @@ class CommonIterator { this._seek(mapped, options) // If _seek() was successfull, more data may be available. - this[kEnded] = false + this.#ended = false } } @@ -236,25 +232,25 @@ class CommonIterator { } async close () { - if (this[kClosingPromise] !== null) { + if (this.#closingPromise !== null) { // First caller of close() is responsible for error - return this[kClosingPromise].catch(noop) + return this.#closingPromise.catch(noop) } // Wrap to avoid race issues on recursive calls - this[kClosingPromise] = new Promise((resolve, reject) => { - this[kPendingClose] = () => { - this[kPendingClose] = null - privateClose(this).then(resolve, reject) + this.#closingPromise = new Promise((resolve, reject) => { + this.#pendingClose = () => { + this.#pendingClose = null + this.#privateClose().then(resolve, reject) } }) // If working we'll delay closing, but still handle the close error (if any) here - if (!this[kWorking]) { - this[kPendingClose]() + if (!this.#working) { + this.#pendingClose() } - return this[kClosingPromise] + return this.#closingPromise } async _close () {} @@ -267,11 +263,50 @@ class CommonIterator { yield item } } catch (err) { - await destroy(this, err) + await this.#destroy(err) } finally { await this.close() } } + + #startWork () { + if (this.#closingPromise !== null) { + throw new ModuleError('Iterator is not open: cannot read after close()', { + code: 'LEVEL_ITERATOR_NOT_OPEN' + }) + } else if (this.#working) { + throw new ModuleError('Iterator is busy: cannot read until previous read has completed', { + code: 'LEVEL_ITERATOR_BUSY' + }) + } else if (this.#signal?.aborted) { + throw new AbortError() + } + + // Keep snapshot open during operation + this.#snapshot?.ref() + this.#working = true + } + + #endWork () { + this.#working = false + this.#pendingClose?.() + this.#snapshot?.unref() + } + + async #privateClose () { + await this._close() + this.db.detachResource(this) + } + + async #destroy (err) { + try { + await this.close() + } catch (closeErr) { + throw combineErrors([err, closeErr]) + } + + throw err + } } if (typeof Symbol.asyncDispose === 'symbol') { @@ -282,10 +317,13 @@ if (typeof Symbol.asyncDispose === 'symbol') { // For backwards compatibility this class is not (yet) called AbstractEntryIterator. class AbstractIterator extends CommonIterator { + #keys + #values + constructor (db, options) { super(db, options) - this[kKeys] = options.keys !== false - this[kValues] = options.values !== false + this.#keys = options.keys !== false + this.#values = options.values !== false } [kDecodeOne] (entry) { @@ -293,11 +331,11 @@ class AbstractIterator extends CommonIterator { const value = entry[1] if (key !== undefined) { - entry[0] = this[kKeys] ? this[kKeyEncoding].decode(key) : undefined + entry[0] = this.#keys ? this[kKeyEncoding].decode(key) : undefined } if (value !== undefined) { - entry[1] = this[kValues] ? this[kValueEncoding].decode(value) : undefined + entry[1] = this.#values ? this[kValueEncoding].decode(value) : undefined } return entry @@ -311,8 +349,8 @@ class AbstractIterator extends CommonIterator { const key = entry[0] const value = entry[1] - if (key !== undefined) entry[0] = this[kKeys] ? keyEncoding.decode(key) : undefined - if (value !== undefined) entry[1] = this[kValues] ? valueEncoding.decode(value) : undefined + if (key !== undefined) entry[0] = this.#keys ? keyEncoding.decode(key) : undefined + if (value !== undefined) entry[1] = this.#values ? valueEncoding.decode(value) : undefined } } } @@ -357,55 +395,6 @@ class IteratorDecodeError extends ModuleError { } } -const startWork = function (iterator) { - if (iterator[kClosingPromise] !== null) { - throw new ModuleError('Iterator is not open: cannot read after close()', { - code: 'LEVEL_ITERATOR_NOT_OPEN' - }) - } else if (iterator[kWorking]) { - throw new ModuleError('Iterator is busy: cannot read until previous read has completed', { - code: 'LEVEL_ITERATOR_BUSY' - }) - } else if (iterator[kSignal] !== null && iterator[kSignal].aborted) { - throw new AbortError() - } - - iterator[kWorking] = true - - // Keep snapshot open during operation - if (iterator[kSnapshot] !== null) { - iterator[kSnapshot].ref() - } -} - -const endWork = function (iterator) { - iterator[kWorking] = false - - if (iterator[kPendingClose] !== null) { - iterator[kPendingClose]() - } - - // Release snapshot - if (iterator[kSnapshot] !== null) { - iterator[kSnapshot].unref() - } -} - -const privateClose = async function (iterator) { - await iterator._close() - iterator.db.detachResource(iterator) -} - -const destroy = async function (iterator, err) { - try { - await iterator.close() - } catch (closeErr) { - throw combineErrors([err, closeErr]) - } - - throw err -} - // Exposed so that AbstractLevel can set these options AbstractIterator.keyEncoding = kKeyEncoding AbstractIterator.valueEncoding = kValueEncoding diff --git a/abstract-level.js b/abstract-level.js index 1f37715..e326a01 100644 --- a/abstract-level.js +++ b/abstract-level.js @@ -17,22 +17,20 @@ const { prefixDescendantKey, isDescendant } = require('./lib/prefixes') const { DeferredQueue } = require('./lib/deferred-queue') const rangeOptions = require('./lib/range-options') -const kResources = Symbol('resources') -const kCloseResources = Symbol('closeResources') -const kQueue = Symbol('queue') -const kDeferOpen = Symbol('deferOpen') -const kOptions = Symbol('options') -const kStatus = Symbol('status') -const kStatusChange = Symbol('statusChange') -const kStatusLocked = Symbol('statusLocked') -const kDefaultOptions = Symbol('defaultOptions') -const kTranscoder = Symbol('transcoder') -const kKeyEncoding = Symbol('keyEncoding') -const kValueEncoding = Symbol('valueEncoding') -const kEventMonitor = Symbol('eventMonitor') -const kArrayBatch = Symbol('arrayBatch') - class AbstractLevel extends EventEmitter { + #status = 'opening' + #deferOpen = true + #statusChange = null + #statusLocked = false + #resources + #queue + #options + #defaultOptions + #transcoder + #keyEncoding + #valueEncoding + #eventMonitor + constructor (manifest, options) { super() @@ -43,13 +41,9 @@ class AbstractLevel extends EventEmitter { options = getOptions(options) const { keyEncoding, valueEncoding, passive, ...forward } = options - this[kResources] = new Set() - this[kQueue] = new DeferredQueue() - this[kDeferOpen] = true - this[kOptions] = forward - this[kStatus] = 'opening' - this[kStatusChange] = null - this[kStatusLocked] = false + this.#resources = new Set() + this.#queue = new DeferredQueue() + this.#options = forward // Aliased for backwards compatibility const implicitSnapshots = manifest.snapshots !== false && @@ -78,39 +72,39 @@ class AbstractLevel extends EventEmitter { }) // Monitor event listeners - this[kEventMonitor] = new EventMonitor(this, [ + this.#eventMonitor = new EventMonitor(this, [ { name: 'write' }, { name: 'put', deprecated: true, alt: 'write' }, { name: 'del', deprecated: true, alt: 'write' }, { name: 'batch', deprecated: true, alt: 'write' } ]) - this[kTranscoder] = new Transcoder(formats(this)) - this[kKeyEncoding] = this[kTranscoder].encoding(keyEncoding || 'utf8') - this[kValueEncoding] = this[kTranscoder].encoding(valueEncoding || 'utf8') + this.#transcoder = new Transcoder(formats(this)) + this.#keyEncoding = this.#transcoder.encoding(keyEncoding || 'utf8') + this.#valueEncoding = this.#transcoder.encoding(valueEncoding || 'utf8') // Add custom and transcoder encodings to manifest - for (const encoding of this[kTranscoder].encodings()) { + for (const encoding of this.#transcoder.encodings()) { if (!this.supports.encodings[encoding.commonName]) { this.supports.encodings[encoding.commonName] = true } } - this[kDefaultOptions] = { + this.#defaultOptions = { empty: emptyOptions, entry: Object.freeze({ - keyEncoding: this[kKeyEncoding].commonName, - valueEncoding: this[kValueEncoding].commonName + keyEncoding: this.#keyEncoding.commonName, + valueEncoding: this.#valueEncoding.commonName }), entryFormat: Object.freeze({ - keyEncoding: this[kKeyEncoding].format, - valueEncoding: this[kValueEncoding].format + keyEncoding: this.#keyEncoding.format, + valueEncoding: this.#valueEncoding.format }), key: Object.freeze({ - keyEncoding: this[kKeyEncoding].commonName + keyEncoding: this.#keyEncoding.commonName }), keyFormat: Object.freeze({ - keyEncoding: this[kKeyEncoding].format + keyEncoding: this.#keyEncoding.format }), owner: Object.freeze({ owner: this @@ -120,14 +114,14 @@ class AbstractLevel extends EventEmitter { // Before we start opening, let subclass finish its constructor // and allow events and postopen hook functions to be added. queueMicrotask(() => { - if (this[kDeferOpen]) { + if (this.#deferOpen) { this.open({ passive: false }).catch(noop) } }) } get status () { - return this[kStatus] + return this.#status } get parent () { @@ -135,15 +129,15 @@ class AbstractLevel extends EventEmitter { } keyEncoding (encoding) { - return this[kTranscoder].encoding(encoding != null ? encoding : this[kKeyEncoding]) + return this.#transcoder.encoding(encoding ?? this.#keyEncoding) } valueEncoding (encoding) { - return this[kTranscoder].encoding(encoding != null ? encoding : this[kValueEncoding]) + return this.#transcoder.encoding(encoding ?? this.#valueEncoding) } async open (options) { - options = { ...this[kOptions], ...getOptions(options) } + options = { ...this.#options, ...getOptions(options) } options.createIfMissing = options.createIfMissing !== false options.errorIfExists = !!options.errorIfExists @@ -152,36 +146,36 @@ class AbstractLevel extends EventEmitter { const postopen = this.hooks.postopen.noop ? null : this.hooks.postopen.run const passive = options.passive - if (passive && this[kDeferOpen]) { + if (passive && this.#deferOpen) { // Wait a tick until constructor calls open() non-passively await undefined } // Wait for pending changes and check that opening is allowed - assertUnlocked(this) - while (this[kStatusChange] !== null) await this[kStatusChange].catch(noop) - assertUnlocked(this) + this.#assertUnlocked() + while (this.#statusChange !== null) await this.#statusChange.catch(noop) + this.#assertUnlocked() if (passive) { - if (this[kStatus] !== 'open') throw new NotOpenError() - } else if (this[kStatus] === 'closed' || this[kDeferOpen]) { - this[kDeferOpen] = false - this[kStatusChange] = resolvedPromise // TODO: refactor - this[kStatusChange] = (async () => { - this[kStatus] = 'opening' + if (this.#status !== 'open') throw new NotOpenError() + } else if (this.#status === 'closed' || this.#deferOpen) { + this.#deferOpen = false + this.#statusChange = resolvedPromise // TODO: refactor + this.#statusChange = (async () => { + this.#status = 'opening' try { this.emit('opening') await this._open(options) } catch (err) { - this[kStatus] = 'closed' + this.#status = 'closed' // Must happen before we close resources, in case their close() is waiting // on a deferred operation which in turn is waiting on db.open(). - this[kQueue].drain() + this.#queue.drain() try { - await this[kCloseResources]() + await this.#closeResources() } catch (resourceErr) { // eslint-disable-next-line no-ex-assign err = combineErrors([err, resourceErr]) @@ -190,38 +184,38 @@ class AbstractLevel extends EventEmitter { throw new NotOpenError(err) } - this[kStatus] = 'open' + this.#status = 'open' if (postopen !== null) { let hookErr try { // Prevent deadlock - this[kStatusLocked] = true + this.#statusLocked = true await postopen(options) } catch (err) { hookErr = convertRejection(err) } finally { - this[kStatusLocked] = false + this.#statusLocked = false } // Revert if (hookErr) { - this[kStatus] = 'closing' - this[kQueue].drain() + this.#status = 'closing' + this.#queue.drain() try { - await this[kCloseResources]() + await this.#closeResources() await this._close() } catch (closeErr) { // There's no safe state to return to. Can't return to 'open' because // postopen hook failed. Can't return to 'closed' (with the ability to // reopen) because the underlying database is potentially still open. - this[kStatusLocked] = true + this.#statusLocked = true hookErr = combineErrors([hookErr, closeErr]) } - this[kStatus] = 'closed' + this.#status = 'closed' throw new ModuleError('The postopen hook failed on open()', { code: 'LEVEL_HOOK_ERROR', @@ -230,16 +224,16 @@ class AbstractLevel extends EventEmitter { } } - this[kQueue].drain() + this.#queue.drain() this.emit('open') })() try { - await this[kStatusChange] + await this.#statusChange } finally { - this[kStatusChange] = null + this.#statusChange = null } - } else if (this[kStatus] !== 'open') { + } else if (this.#status !== 'open') { /* istanbul ignore next: should not happen */ throw new NotOpenError() } @@ -249,53 +243,53 @@ class AbstractLevel extends EventEmitter { async close () { // Wait for pending changes and check that closing is allowed - assertUnlocked(this) - while (this[kStatusChange] !== null) await this[kStatusChange].catch(noop) - assertUnlocked(this) + this.#assertUnlocked() + while (this.#statusChange !== null) await this.#statusChange.catch(noop) + this.#assertUnlocked() - if (this[kStatus] === 'open' || this[kDeferOpen]) { + if (this.#status === 'open' || this.#deferOpen) { // If close() was called after constructor, we didn't open yet - const fromInitial = this[kDeferOpen] + const fromInitial = this.#deferOpen - this[kDeferOpen] = false - this[kStatusChange] = resolvedPromise - this[kStatusChange] = (async () => { - this[kStatus] = 'closing' - this[kQueue].drain() + this.#deferOpen = false + this.#statusChange = resolvedPromise + this.#statusChange = (async () => { + this.#status = 'closing' + this.#queue.drain() try { this.emit('closing') - await this[kCloseResources]() + await this.#closeResources() if (!fromInitial) await this._close() } catch (err) { - this[kStatus] = 'open' - this[kQueue].drain() + this.#status = 'open' + this.#queue.drain() throw new NotClosedError(err) } - this[kStatus] = 'closed' - this[kQueue].drain() + this.#status = 'closed' + this.#queue.drain() this.emit('closed') })() try { - await this[kStatusChange] + await this.#statusChange } finally { - this[kStatusChange] = null + this.#statusChange = null } - } else if (this[kStatus] !== 'closed') { + } else if (this.#status !== 'closed') { /* istanbul ignore next: should not happen */ throw new NotClosedError() } } - async [kCloseResources] () { - if (this[kResources].size === 0) { + async #closeResources () { + if (this.#resources.size === 0) { return } // In parallel so that all resources know they are closed - const resources = Array.from(this[kResources]) + const resources = Array.from(this.#resources) const promises = resources.map(closeResource) // TODO: async/await @@ -304,7 +298,7 @@ class AbstractLevel extends EventEmitter { for (let i = 0; i < results.length; i++) { if (results[i].status === 'fulfilled') { - this[kResources].delete(resources[i]) + this.#resources.delete(resources[i]) } else { errors.push(convertRejection(results[i].reason)) } @@ -319,18 +313,18 @@ class AbstractLevel extends EventEmitter { async _close () {} async get (key, options) { - options = getOptions(options, this[kDefaultOptions].entry) + options = getOptions(options, this.#defaultOptions.entry) - if (this[kStatus] === 'opening') { + if (this.#status === 'opening') { return this.deferAsync(() => this.get(key, options)) } - assertOpen(this) + this.#assertOpen() const err = this._checkKey(key) if (err) throw err - const snapshot = options.snapshot != null ? options.snapshot : null + const snapshot = options.snapshot const keyEncoding = this.keyEncoding(options.keyEncoding) const valueEncoding = this.valueEncoding(options.valueEncoding) const keyFormat = keyEncoding.format @@ -346,9 +340,7 @@ class AbstractLevel extends EventEmitter { const mappedKey = this.prefixKey(encodedKey, keyFormat, true) // Keep snapshot open during operation - if (snapshot !== null) { - snapshot.ref() - } + snapshot?.ref() let value @@ -356,9 +348,7 @@ class AbstractLevel extends EventEmitter { value = await this._get(mappedKey, options) } finally { // Release snapshot - if (snapshot !== null) { - snapshot.unref() - } + snapshot?.unref() } try { @@ -376,13 +366,13 @@ class AbstractLevel extends EventEmitter { } async getMany (keys, options) { - options = getOptions(options, this[kDefaultOptions].entry) + options = getOptions(options, this.#defaultOptions.entry) - if (this[kStatus] === 'opening') { + if (this.#status === 'opening') { return this.deferAsync(() => this.getMany(keys, options)) } - assertOpen(this) + this.#assertOpen() if (!Array.isArray(keys)) { throw new TypeError("The first argument 'keys' must be an array") @@ -392,7 +382,7 @@ class AbstractLevel extends EventEmitter { return [] } - const snapshot = options.snapshot != null ? options.snapshot : null + const snapshot = options.snapshot const keyEncoding = this.keyEncoding(options.keyEncoding) const valueEncoding = this.valueEncoding(options.valueEncoding) const keyFormat = keyEncoding.format @@ -414,9 +404,7 @@ class AbstractLevel extends EventEmitter { } // Keep snapshot open during operation - if (snapshot !== null) { - snapshot.ref() - } + snapshot?.ref() let values @@ -424,9 +412,7 @@ class AbstractLevel extends EventEmitter { values = await this._getMany(mappedKeys, options) } finally { // Release snapshot - if (snapshot !== null) { - snapshot.unref() - } + snapshot?.unref() } try { @@ -457,13 +443,13 @@ class AbstractLevel extends EventEmitter { return this.batch([{ type: 'put', key, value }], options) } - options = getOptions(options, this[kDefaultOptions].entry) + options = getOptions(options, this.#defaultOptions.entry) - if (this[kStatus] === 'opening') { + if (this.#status === 'opening') { return this.deferAsync(() => this.put(key, value, options)) } - assertOpen(this) + this.#assertOpen() const err = this._checkKey(key) || this._checkValue(value) if (err) throw err @@ -473,13 +459,13 @@ class AbstractLevel extends EventEmitter { const valueEncoding = this.valueEncoding(options.valueEncoding) const keyFormat = keyEncoding.format const valueFormat = valueEncoding.format - const enableWriteEvent = this[kEventMonitor].write + const enableWriteEvent = this.#eventMonitor.write const original = options // Avoid Object.assign() for default options // TODO: also apply this tweak to get() and getMany() - if (options === this[kDefaultOptions].entry) { - options = this[kDefaultOptions].entryFormat + if (options === this.#defaultOptions.entry) { + options = this.#defaultOptions.entryFormat } else if (options.keyEncoding !== keyFormat || options.valueEncoding !== valueFormat) { options = Object.assign({}, options, { keyEncoding: keyFormat, valueEncoding: valueFormat }) } @@ -516,13 +502,13 @@ class AbstractLevel extends EventEmitter { return this.batch([{ type: 'del', key }], options) } - options = getOptions(options, this[kDefaultOptions].key) + options = getOptions(options, this.#defaultOptions.key) - if (this[kStatus] === 'opening') { + if (this.#status === 'opening') { return this.deferAsync(() => this.del(key, options)) } - assertOpen(this) + this.#assertOpen() const err = this._checkKey(key) if (err) throw err @@ -530,12 +516,12 @@ class AbstractLevel extends EventEmitter { // Encode data for private API const keyEncoding = this.keyEncoding(options.keyEncoding) const keyFormat = keyEncoding.format - const enableWriteEvent = this[kEventMonitor].write + const enableWriteEvent = this.#eventMonitor.write const original = options // Avoid Object.assign() for default options - if (options === this[kDefaultOptions].key) { - options = this[kDefaultOptions].keyFormat + if (options === this.#defaultOptions.key) { + options = this.#defaultOptions.keyFormat } else if (options.keyEncoding !== keyFormat) { options = Object.assign({}, options, { keyEncoding: keyFormat }) } @@ -567,22 +553,22 @@ class AbstractLevel extends EventEmitter { // of classic-level, that should not be copied to individual operations. batch (operations, options) { if (!arguments.length) { - assertOpen(this) + this.#assertOpen() return this._chainedBatch() } - options = getOptions(options, this[kDefaultOptions].empty) - return this[kArrayBatch](operations, options) + options = getOptions(options, this.#defaultOptions.empty) + return this.#arrayBatch(operations, options) } // Wrapped for async error handling - async [kArrayBatch] (operations, options) { + async #arrayBatch (operations, options) { // TODO (not urgent): freeze prewrite hook and write event - if (this[kStatus] === 'opening') { - return this.deferAsync(() => this[kArrayBatch](operations, options)) + if (this.#status === 'opening') { + return this.deferAsync(() => this.#arrayBatch(operations, options)) } - assertOpen(this) + this.#assertOpen() if (!Array.isArray(operations)) { throw new TypeError("The first argument 'operations' must be an array") @@ -594,7 +580,7 @@ class AbstractLevel extends EventEmitter { const length = operations.length const enablePrewriteHook = !this.hooks.prewrite.noop - const enableWriteEvent = this[kEventMonitor].write + const enableWriteEvent = this.#eventMonitor.write const publicOperations = enableWriteEvent ? new Array(length) : null const privateOperations = new Array(length) const prewriteBatch = enablePrewriteHook @@ -745,34 +731,30 @@ class AbstractLevel extends EventEmitter { } async clear (options) { - options = getOptions(options, this[kDefaultOptions].empty) + options = getOptions(options, this.#defaultOptions.empty) - if (this[kStatus] === 'opening') { + if (this.#status === 'opening') { return this.deferAsync(() => this.clear(options)) } - assertOpen(this) + this.#assertOpen() const original = options const keyEncoding = this.keyEncoding(options.keyEncoding) - const snapshot = options.snapshot != null ? options.snapshot : null + const snapshot = options.snapshot options = rangeOptions(options, keyEncoding) options.keyEncoding = keyEncoding.format if (options.limit !== 0) { // Keep snapshot open during operation - if (snapshot !== null) { - snapshot.ref() - } + snapshot?.ref() try { await this._clear(options) } finally { // Release snapshot - if (snapshot !== null) { - snapshot.unref() - } + snapshot?.unref() } this.emit('clear', original) @@ -782,8 +764,8 @@ class AbstractLevel extends EventEmitter { async _clear (options) {} iterator (options) { - const keyEncoding = this.keyEncoding(options && options.keyEncoding) - const valueEncoding = this.valueEncoding(options && options.valueEncoding) + const keyEncoding = this.keyEncoding(options?.keyEncoding) + const valueEncoding = this.valueEncoding(options?.valueEncoding) options = rangeOptions(options, keyEncoding) options.keys = options.keys !== false @@ -797,11 +779,11 @@ class AbstractLevel extends EventEmitter { options.keyEncoding = keyEncoding.format options.valueEncoding = valueEncoding.format - if (this[kStatus] === 'opening') { + if (this.#status === 'opening') { return new DeferredIterator(this, options) } - assertOpen(this) + this.#assertOpen() return this._iterator(options) } @@ -811,8 +793,8 @@ class AbstractLevel extends EventEmitter { keys (options) { // Also include valueEncoding (though unused) because we may fallback to _iterator() - const keyEncoding = this.keyEncoding(options && options.keyEncoding) - const valueEncoding = this.valueEncoding(options && options.valueEncoding) + const keyEncoding = this.keyEncoding(options?.keyEncoding) + const valueEncoding = this.valueEncoding(options?.valueEncoding) options = rangeOptions(options, keyEncoding) @@ -824,11 +806,11 @@ class AbstractLevel extends EventEmitter { options.keyEncoding = keyEncoding.format options.valueEncoding = valueEncoding.format - if (this[kStatus] === 'opening') { + if (this.#status === 'opening') { return new DeferredKeyIterator(this, options) } - assertOpen(this) + this.#assertOpen() return this._keys(options) } @@ -837,8 +819,8 @@ class AbstractLevel extends EventEmitter { } values (options) { - const keyEncoding = this.keyEncoding(options && options.keyEncoding) - const valueEncoding = this.valueEncoding(options && options.valueEncoding) + const keyEncoding = this.keyEncoding(options?.keyEncoding) + const valueEncoding = this.valueEncoding(options?.valueEncoding) options = rangeOptions(options, keyEncoding) @@ -850,11 +832,11 @@ class AbstractLevel extends EventEmitter { options.keyEncoding = keyEncoding.format options.valueEncoding = valueEncoding.format - if (this[kStatus] === 'opening') { + if (this.#status === 'opening') { return new DeferredValueIterator(this, options) } - assertOpen(this) + this.#assertOpen() return this._values(options) } @@ -863,11 +845,11 @@ class AbstractLevel extends EventEmitter { } snapshot (options) { - assertOpen(this) + this.#assertOpen() // Owner is an undocumented option explained in AbstractSnapshot if (typeof options !== 'object' || options === null) { - options = this[kDefaultOptions].owner + options = this.#defaultOptions.owner } else if (options.owner == null) { options = { ...options, owner: this } } @@ -886,7 +868,7 @@ class AbstractLevel extends EventEmitter { throw new TypeError('The first argument must be a function') } - this[kQueue].add(function (abortError) { + this.#queue.add(function (abortError) { if (!abortError) fn() }, options) } @@ -897,7 +879,7 @@ class AbstractLevel extends EventEmitter { } return new Promise((resolve, reject) => { - this[kQueue].add(function (abortError) { + this.#queue.add(function (abortError) { if (abortError) reject(abortError) else fn().then(resolve, reject) }, options) @@ -911,12 +893,12 @@ class AbstractLevel extends EventEmitter { throw new TypeError('The first argument must be a resource object') } - this[kResources].add(resource) + this.#resources.add(resource) } // TODO: docs and types detachResource (resource) { - this[kResources].delete(resource) + this.#resources.delete(resource) } _chainedBatch () { @@ -938,6 +920,22 @@ class AbstractLevel extends EventEmitter { }) } } + + #assertOpen () { + if (this.#status !== 'open') { + throw new ModuleError('Database is not open', { + code: 'LEVEL_DATABASE_NOT_OPEN' + }) + } + } + + #assertUnlocked () { + if (this.#statusLocked) { + throw new ModuleError('Database status is locked', { + code: 'LEVEL_STATUS_LOCKED' + }) + } + } } const { AbstractSublevel } = require('./lib/abstract-sublevel')({ AbstractLevel }) @@ -951,22 +949,6 @@ if (typeof Symbol.asyncDispose === 'symbol') { } } -const assertOpen = function (db) { - if (db[kStatus] !== 'open') { - throw new ModuleError('Database is not open', { - code: 'LEVEL_DATABASE_NOT_OPEN' - }) - } -} - -const assertUnlocked = function (db) { - if (db[kStatusLocked]) { - throw new ModuleError('Database status is locked', { - code: 'LEVEL_STATUS_LOCKED' - }) - } -} - const formats = function (db) { return Object.keys(db.supports.encodings) .filter(k => !!db.supports.encodings[k]) diff --git a/abstract-snapshot.js b/abstract-snapshot.js index 4ac9c09..db365a9 100644 --- a/abstract-snapshot.js +++ b/abstract-snapshot.js @@ -38,8 +38,8 @@ class AbstractSnapshot { } unref () { - if (--this.#referenceCount === 0 && this.#pendingClose !== null) { - this.#pendingClose() + if (--this.#referenceCount === 0) { + this.#pendingClose?.() } } diff --git a/lib/abstract-sublevel-iterator.js b/lib/abstract-sublevel-iterator.js index 1533565..cca33c8 100644 --- a/lib/abstract-sublevel-iterator.js +++ b/lib/abstract-sublevel-iterator.js @@ -2,32 +2,32 @@ const { AbstractIterator, AbstractKeyIterator, AbstractValueIterator } = require('../abstract-iterator') -const kUnfix = Symbol('unfix') -const kIterator = Symbol('iterator') - // TODO: unfix natively if db supports it class AbstractSublevelIterator extends AbstractIterator { + #iterator + #unfix + constructor (db, options, iterator, unfix) { super(db, options) - this[kIterator] = iterator - this[kUnfix] = unfix + this.#iterator = iterator + this.#unfix = unfix } async _next () { - const entry = await this[kIterator].next() + const entry = await this.#iterator.next() if (entry !== undefined) { const key = entry[0] - if (key !== undefined) entry[0] = this[kUnfix](key) + if (key !== undefined) entry[0] = this.#unfix(key) } return entry } async _nextv (size, options) { - const entries = await this[kIterator].nextv(size, options) - const unfix = this[kUnfix] + const entries = await this.#iterator.nextv(size, options) + const unfix = this.#unfix for (const entry of entries) { const key = entry[0] @@ -38,8 +38,8 @@ class AbstractSublevelIterator extends AbstractIterator { } async _all (options) { - const entries = await this[kIterator].all(options) - const unfix = this[kUnfix] + const entries = await this.#iterator.all(options) + const unfix = this.#unfix for (const entry of entries) { const key = entry[0] @@ -48,24 +48,35 @@ class AbstractSublevelIterator extends AbstractIterator { return entries } + + _seek (target, options) { + this.#iterator.seek(target, options) + } + + async _close () { + return this.#iterator.close() + } } class AbstractSublevelKeyIterator extends AbstractKeyIterator { + #iterator + #unfix + constructor (db, options, iterator, unfix) { super(db, options) - this[kIterator] = iterator - this[kUnfix] = unfix + this.#iterator = iterator + this.#unfix = unfix } async _next () { - const key = await this[kIterator].next() - return key === undefined ? key : this[kUnfix](key) + const key = await this.#iterator.next() + return key === undefined ? key : this.#unfix(key) } async _nextv (size, options) { - const keys = await this[kIterator].nextv(size, options) - const unfix = this[kUnfix] + const keys = await this.#iterator.nextv(size, options) + const unfix = this.#unfix for (let i = 0; i < keys.length; i++) { const key = keys[i] @@ -76,8 +87,8 @@ class AbstractSublevelKeyIterator extends AbstractKeyIterator { } async _all (options) { - const keys = await this[kIterator].all(options) - const unfix = this[kUnfix] + const keys = await this.#iterator.all(options) + const unfix = this.#unfix for (let i = 0; i < keys.length; i++) { const key = keys[i] @@ -86,34 +97,42 @@ class AbstractSublevelKeyIterator extends AbstractKeyIterator { return keys } + + _seek (target, options) { + this.#iterator.seek(target, options) + } + + async _close () { + return this.#iterator.close() + } } class AbstractSublevelValueIterator extends AbstractValueIterator { + #iterator + constructor (db, options, iterator) { super(db, options) - this[kIterator] = iterator + this.#iterator = iterator } async _next () { - return this[kIterator].next() + return this.#iterator.next() } async _nextv (size, options) { - return this[kIterator].nextv(size, options) + return this.#iterator.nextv(size, options) } async _all (options) { - return this[kIterator].all(options) + return this.#iterator.all(options) } -} -for (const Iterator of [AbstractSublevelIterator, AbstractSublevelKeyIterator, AbstractSublevelValueIterator]) { - Iterator.prototype._seek = function (target, options) { - this[kIterator].seek(target, options) + _seek (target, options) { + this.#iterator.seek(target, options) } - Iterator.prototype._close = async function () { - return this[kIterator].close() + async _close () { + return this.#iterator.close() } } diff --git a/lib/abstract-sublevel.js b/lib/abstract-sublevel.js index 4b40893..7079965 100644 --- a/lib/abstract-sublevel.js +++ b/lib/abstract-sublevel.js @@ -8,22 +8,21 @@ const { AbstractSublevelValueIterator } = require('./abstract-sublevel-iterator') -const kGlobalPrefix = Symbol('prefix') -const kLocalPrefix = Symbol('localPrefix') -const kLocalPath = Symbol('localPath') -const kGlobalPath = Symbol('globalPath') -const kGlobalUpperBound = Symbol('upperBound') -const kPrefixRange = Symbol('prefixRange') const kRoot = Symbol('root') -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 { + #globalPrefix + #localPrefix + #localPath + #globalPath + #globalUpperBound + #parent + #unfix + static defaults (options) { if (options == null) { return defaults @@ -62,17 +61,17 @@ module.exports = function ({ AbstractLevel }) { // still forward to the root database - which is older logic and does not yet need // to change, until we add some form of preread or postread hooks. this[kRoot] = root - this[kParent] = db - this[kLocalPath] = names - this[kGlobalPath] = db.prefix ? db.path().concat(names) : names - this[kGlobalPrefix] = new MultiFormat(globalPrefix) - this[kGlobalUpperBound] = new MultiFormat(globalUpperBound) - this[kLocalPrefix] = new MultiFormat(localPrefix) - this[kUnfix] = new Unfixer() + this.#parent = db + this.#localPath = names + this.#globalPath = db.prefix ? db.path().concat(names) : names + this.#globalPrefix = new MultiFormat(globalPrefix) + this.#globalUpperBound = new MultiFormat(globalUpperBound) + this.#localPrefix = new MultiFormat(localPrefix) + this.#unfix = new Unfixer() } prefixKey (key, keyFormat, local) { - const prefix = local ? this[kLocalPrefix] : this[kGlobalPrefix] + const prefix = local ? this.#localPrefix : this.#globalPrefix if (keyFormat === 'utf8') { return prefix.utf8 + key @@ -94,13 +93,13 @@ module.exports = function ({ AbstractLevel }) { } // Not exposed for now. - [kPrefixRange] (range, keyFormat) { + #prefixRange (range, keyFormat) { if (range.gte !== undefined) { range.gte = this.prefixKey(range.gte, keyFormat, false) } else if (range.gt !== undefined) { range.gt = this.prefixKey(range.gt, keyFormat, false) } else { - range.gte = this[kGlobalPrefix][keyFormat] + range.gte = this.#globalPrefix[keyFormat] } if (range.lte !== undefined) { @@ -108,12 +107,12 @@ module.exports = function ({ AbstractLevel }) { } else if (range.lt !== undefined) { range.lt = this.prefixKey(range.lt, keyFormat, false) } else { - range.lte = this[kGlobalUpperBound][keyFormat] + range.lte = this.#globalUpperBound[keyFormat] } } get prefix () { - return this[kGlobalPrefix].utf8 + return this.#globalPrefix.utf8 } get db () { @@ -121,64 +120,64 @@ module.exports = function ({ AbstractLevel }) { } get parent () { - return this[kParent] + return this.#parent } path (local = false) { - return local ? this[kLocalPath] : this[kGlobalPath] + return local ? this.#localPath : this.#globalPath } async _open (options) { // 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. - return this[kParent].open({ passive: true }) + return this.#parent.open({ passive: true }) } async _put (key, value, options) { - return this[kParent].put(key, value, options) + return this.#parent.put(key, value, options) } async _get (key, options) { - return this[kParent].get(key, options) + return this.#parent.get(key, options) } async _getMany (keys, options) { - return this[kParent].getMany(keys, options) + return this.#parent.getMany(keys, options) } async _del (key, options) { - return this[kParent].del(key, options) + return this.#parent.del(key, options) } async _batch (operations, options) { - return this[kParent].batch(operations, options) + return this.#parent.batch(operations, options) } // TODO: call parent instead of root async _clear (options) { // TODO (refactor): move to AbstractLevel - this[kPrefixRange](options, options.keyEncoding) + this.#prefixRange(options, options.keyEncoding) return this[kRoot].clear(options) } // TODO: call parent instead of root _iterator (options) { // TODO (refactor): move to AbstractLevel - this[kPrefixRange](options, options.keyEncoding) + this.#prefixRange(options, options.keyEncoding) const iterator = this[kRoot].iterator(options) - const unfix = this[kUnfix].get(this[kGlobalPrefix].utf8.length, options.keyEncoding) + const unfix = this.#unfix.get(this.#globalPrefix.utf8.length, options.keyEncoding) return new AbstractSublevelIterator(this, options, iterator, unfix) } _keys (options) { - this[kPrefixRange](options, options.keyEncoding) + this.#prefixRange(options, options.keyEncoding) const iterator = this[kRoot].keys(options) - const unfix = this[kUnfix].get(this[kGlobalPrefix].utf8.length, options.keyEncoding) + const unfix = this.#unfix.get(this.#globalPrefix.utf8.length, options.keyEncoding) return new AbstractSublevelKeyIterator(this, options, iterator, unfix) } _values (options) { - this[kPrefixRange](options, options.keyEncoding) + this.#prefixRange(options, options.keyEncoding) const iterator = this[kRoot].values(options) return new AbstractSublevelValueIterator(this, options, iterator) } diff --git a/lib/default-chained-batch.js b/lib/default-chained-batch.js index 8fbe0c4..659eb86 100644 --- a/lib/default-chained-batch.js +++ b/lib/default-chained-batch.js @@ -1,28 +1,28 @@ 'use strict' const { AbstractChainedBatch } = require('../abstract-chained-batch') -const kEncoded = Symbol('encoded') // Functional default for chained batch class DefaultChainedBatch extends AbstractChainedBatch { + #encoded = [] + constructor (db) { // Opt-in to _add() instead of _put() and _del() super(db, { add: true }) - this[kEncoded] = [] } _add (op) { - this[kEncoded].push(op) + this.#encoded.push(op) } _clear () { - this[kEncoded] = [] + this.#encoded = [] } async _write (options) { // Need to call the private rather than public method, to prevent // recursion, double prefixing, double encoding and double hooks. - return this.db._batch(this[kEncoded], options) + return this.db._batch(this.#encoded, options) } } diff --git a/lib/deferred-queue.js b/lib/deferred-queue.js index b11454a..83805c1 100644 --- a/lib/deferred-queue.js +++ b/lib/deferred-queue.js @@ -3,10 +3,6 @@ const { getOptions, emptyOptions } = require('./common') const { AbortError } = require('./errors') -const kOperations = Symbol('operations') -const kSignals = Symbol('signals') -const kHandleAbort = Symbol('handleAbort') - class DeferredOperation { constructor (fn, signal) { this.fn = fn @@ -15,10 +11,12 @@ class DeferredOperation { } class DeferredQueue { + #operations + #signals + constructor () { - this[kOperations] = [] - this[kSignals] = new Set() - this[kHandleAbort] = this[kHandleAbort].bind(this) + this.#operations = [] + this.#signals = new Set() } add (fn, options) { @@ -26,7 +24,7 @@ class DeferredQueue { const signal = options.signal if (signal == null) { - this[kOperations].push(new DeferredOperation(fn, null)) + this.#operations.push(new DeferredOperation(fn, null)) return } @@ -36,23 +34,23 @@ class DeferredQueue { return } - if (!this[kSignals].has(signal)) { - this[kSignals].add(signal) - signal.addEventListener('abort', this[kHandleAbort], { once: true }) + if (!this.#signals.has(signal)) { + this.#signals.add(signal) + signal.addEventListener('abort', this.#handleAbort, { once: true }) } - this[kOperations].push(new DeferredOperation(fn, signal)) + this.#operations.push(new DeferredOperation(fn, signal)) } drain () { - const operations = this[kOperations] - const signals = this[kSignals] + const operations = this.#operations + const signals = this.#signals - this[kOperations] = [] - this[kSignals] = new Set() + this.#operations = [] + this.#signals = new Set() for (const signal of signals) { - signal.removeEventListener('abort', this[kHandleAbort]) + signal.removeEventListener('abort', this.#handleAbort) } for (const operation of operations) { @@ -60,13 +58,13 @@ class DeferredQueue { } } - [kHandleAbort] (ev) { + #handleAbort = (ev) => { const signal = ev.target const err = new AbortError() const aborted = [] // TODO: optimize - this[kOperations] = this[kOperations].filter(function (operation) { + this.#operations = this.#operations.filter(function (operation) { if (operation.signal !== null && operation.signal === signal) { aborted.push(operation) return false @@ -75,7 +73,7 @@ class DeferredQueue { } }) - this[kSignals].delete(signal) + this.#signals.delete(signal) for (const operation of aborted) { operation.fn.call(null, err) diff --git a/lib/hooks.js b/lib/hooks.js index 3468c94..4afa567 100644 --- a/lib/hooks.js +++ b/lib/hooks.js @@ -2,9 +2,6 @@ const { noop } = require('./common') -const kFunctions = Symbol('functions') -const kAsync = Symbol('async') - class DatabaseHooks { constructor () { this.postopen = new Hook({ async: true }) @@ -14,65 +11,67 @@ class DatabaseHooks { } class Hook { + #functions = new Set() + #isAsync + constructor (options) { - this[kAsync] = options.async - this[kFunctions] = new Set() + this.#isAsync = options.async // Offer a fast way to check if hook functions are present. We could also expose a // size getter, which would be slower, or check it by hook.run !== noop, which would // not allow userland to do the same check. this.noop = true - this.run = runner(this) + this.run = this.#runner() } add (fn) { // Validate now rather than in asynchronous code paths assertFunction(fn) - this[kFunctions].add(fn) + this.#functions.add(fn) this.noop = false - this.run = runner(this) + this.run = this.#runner() } delete (fn) { assertFunction(fn) - this[kFunctions].delete(fn) - this.noop = this[kFunctions].size === 0 - this.run = runner(this) - } -} - -const assertFunction = function (fn) { - if (typeof fn !== 'function') { - const hint = fn === null ? 'null' : typeof fn - throw new TypeError(`The first argument must be a function, received ${hint}`) + this.#functions.delete(fn) + this.noop = this.#functions.size === 0 + this.run = this.#runner() } -} -const runner = function (hook) { - if (hook.noop) { - return noop - } else if (hook[kFunctions].size === 1) { - const [fn] = hook[kFunctions] - return fn - } else if (hook[kAsync]) { - // The run function should not reference hook, so that consumers like chained batch - // and db.open() can save a reference to hook.run and safely assume it won't change - // during their lifetime or async work. - const run = async function (functions, ...args) { - for (const fn of functions) { - await fn(...args) + #runner () { + if (this.noop) { + return noop + } else if (this.#functions.size === 1) { + const [fn] = this.#functions + return fn + } else if (this.#isAsync) { + // The run function should not reference hook, so that consumers like chained batch + // and db.open() can save a reference to hook.run and safely assume it won't change + // during their lifetime or async work. + const run = async function (functions, ...args) { + for (const fn of functions) { + await fn(...args) + } } - } - return run.bind(null, Array.from(hook[kFunctions])) - } else { - const run = function (functions, ...args) { - for (const fn of functions) { - fn(...args) + return run.bind(null, Array.from(this.#functions)) + } else { + const run = function (functions, ...args) { + for (const fn of functions) { + fn(...args) + } } + + return run.bind(null, Array.from(this.#functions)) } + } +} - return run.bind(null, Array.from(hook[kFunctions])) +const assertFunction = function (fn) { + if (typeof fn !== 'function') { + const hint = fn === null ? 'null' : typeof fn + throw new TypeError(`The first argument must be a function, received ${hint}`) } } diff --git a/lib/prewrite-batch.js b/lib/prewrite-batch.js index f71a114..3286c10 100644 --- a/lib/prewrite-batch.js +++ b/lib/prewrite-batch.js @@ -2,25 +2,25 @@ const { prefixDescendantKey, isDescendant } = require('./prefixes') -const kDb = Symbol('db') -const kPrivateOperations = Symbol('privateOperations') -const kPublicOperations = Symbol('publicOperations') - // An interface for prewrite hook functions to add operations class PrewriteBatch { + #db + #privateOperations + #publicOperations + constructor (db, privateOperations, publicOperations) { - this[kDb] = db + this.#db = db // Note: if for db.batch([]), these arrays include input operations (or empty slots // for them) but if for chained batch then it does not. Small implementation detail. - this[kPrivateOperations] = privateOperations - this[kPublicOperations] = publicOperations + this.#privateOperations = privateOperations + this.#publicOperations = publicOperations } add (op) { const isPut = op.type === 'put' const delegated = op.sublevel != null - const db = delegated ? op.sublevel : this[kDb] + const db = delegated ? op.sublevel : this.#db const keyError = db._checkKey(op.key) if (keyError != null) throw keyError @@ -43,9 +43,9 @@ class PrewriteBatch { // If the sublevel is not a descendant then forward that option to the parent db // so that we don't erroneously add our own prefix to the key of the operation. - const siblings = delegated && !isDescendant(op.sublevel, this[kDb]) && op.sublevel !== this[kDb] + const siblings = delegated && !isDescendant(op.sublevel, this.#db) && op.sublevel !== this.#db const encodedKey = delegated && !siblings - ? prefixDescendantKey(preencodedKey, keyFormat, db, this[kDb]) + ? prefixDescendantKey(preencodedKey, keyFormat, db, this.#db) : preencodedKey // Only prefix once @@ -56,22 +56,22 @@ class PrewriteBatch { let publicOperation = null // If the sublevel is not a descendant then we shouldn't emit events - if (this[kPublicOperations] !== null && !siblings) { + if (this.#publicOperations !== null && !siblings) { // Clone op before we mutate it for the private API publicOperation = Object.assign({}, op) publicOperation.encodedKey = encodedKey if (delegated) { - // Ensure emitted data makes sense in the context of this[kDb] + // Ensure emitted data makes sense in the context of this.#db publicOperation.key = encodedKey - publicOperation.keyEncoding = this[kDb].keyEncoding(keyFormat) + publicOperation.keyEncoding = this.#db.keyEncoding(keyFormat) } - this[kPublicOperations].push(publicOperation) + this.#publicOperations.push(publicOperation) } // If we're forwarding the sublevel option then don't prefix the key yet - op.key = siblings ? encodedKey : this[kDb].prefixKey(encodedKey, keyFormat, true) + op.key = siblings ? encodedKey : this.#db.prefixKey(encodedKey, keyFormat, true) op.keyEncoding = keyFormat if (isPut) { @@ -87,12 +87,12 @@ class PrewriteBatch { if (delegated) { publicOperation.value = encodedValue - publicOperation.valueEncoding = this[kDb].valueEncoding(valueFormat) + publicOperation.valueEncoding = this.#db.valueEncoding(valueFormat) } } } - this[kPrivateOperations].push(op) + this.#privateOperations.push(op) return this } } diff --git a/package.json b/package.json index e76d4e3..a7e6334 100644 --- a/package.json +++ b/package.json @@ -34,11 +34,13 @@ "module-error": "^1.0.1" }, "devDependencies": { + "@babel/preset-env": "^7.26.0", "@types/node": "^22.7.7", "@voxpelli/tsconfig": "^15.0.0", "airtap": "^4.0.4", "airtap-electron": "^1.0.0", "airtap-playwright": "^1.0.1", + "babelify": "^10.0.0", "electron": "^30.5.1", "hallmark": "^5.0.1", "nyc": "^15.1.0",