From 092fd4f55718c45a6a0f40942d6fea38c3234a26 Mon Sep 17 00:00:00 2001 From: Vincent Weevers Date: Fri, 27 Dec 2024 16:02:04 +0100 Subject: [PATCH] Use private properties, optional chaining & coalescing operator Just a syntactic refactoring, makes no difference for consumers. The syntax is supported by all our target environments, but if browserify is used (without babel or similar) it'll fail to parse. Not sure if that constitutes a breaking change. Category: change --- .airtap.yml | 5 + .github/dependabot.yml | 4 +- abstract-chained-batch.js | 202 ++++++++--------- abstract-iterator.js | 213 +++++++++--------- abstract-level.js | 362 ++++++++++++++---------------- abstract-snapshot.js | 4 +- lib/abstract-sublevel-iterator.js | 77 ++++--- lib/abstract-sublevel.js | 73 +++--- lib/default-chained-batch.js | 10 +- lib/deferred-queue.js | 38 ++-- lib/hooks.js | 77 ++++--- lib/prewrite-batch.js | 34 +-- package.json | 2 + 13 files changed, 544 insertions(+), 557 deletions(-) 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 74f67af..1b1fbf4 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 { @@ -450,26 +436,26 @@ class AbstractLevel extends EventEmitter { } async has (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.has(key, options)) } - assertOpen(this) + this.#assertOpen() // TODO (next major): change this to an assert 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 keyFormat = keyEncoding.format // Forward encoding options to the underlying store - if (options === this[kDefaultOptions].key) { + if (options === this.#defaultOptions.key) { // Avoid Object.assign() for default options - options = this[kDefaultOptions].keyFormat + options = this.#defaultOptions.keyFormat } else if (options.keyEncoding !== keyFormat) { // Avoid spread operator because of https://bugs.chromium.org/p/chromium/issues/detail?id=1204540 options = Object.assign({}, options, { keyEncoding: keyFormat }) @@ -479,17 +465,13 @@ class AbstractLevel extends EventEmitter { const mappedKey = this.prefixKey(encodedKey, keyFormat, true) // Keep snapshot open during operation - if (snapshot !== null) { - snapshot.ref() - } + snapshot?.ref() try { return this._has(mappedKey, options) } finally { // Release snapshot - if (snapshot !== null) { - snapshot.unref() - } + snapshot?.unref() } } @@ -500,13 +482,13 @@ class AbstractLevel extends EventEmitter { } async hasMany (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.hasMany(keys, options)) } - assertOpen(this) + this.#assertOpen() if (!Array.isArray(keys)) { throw new TypeError("The first argument 'keys' must be an array") @@ -516,14 +498,14 @@ class AbstractLevel extends EventEmitter { return [] } - const snapshot = options.snapshot != null ? options.snapshot : null + const snapshot = options.snapshot const keyEncoding = this.keyEncoding(options.keyEncoding) const keyFormat = keyEncoding.format // Forward encoding options to the underlying store - if (options === this[kDefaultOptions].key) { + if (options === this.#defaultOptions.key) { // Avoid Object.assign() for default options - options = this[kDefaultOptions].keyFormat + options = this.#defaultOptions.keyFormat } else if (options.keyEncoding !== keyFormat) { // Avoid spread operator because of https://bugs.chromium.org/p/chromium/issues/detail?id=1204540 options = Object.assign({}, options, { keyEncoding: keyFormat }) @@ -540,17 +522,13 @@ class AbstractLevel extends EventEmitter { } // Keep snapshot open during operation - if (snapshot !== null) { - snapshot.ref() - } + snapshot?.ref() try { return this._hasMany(mappedKeys, options) } finally { // Release snapshot - if (snapshot !== null) { - snapshot.unref() - } + snapshot?.unref() } } @@ -568,13 +546,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 @@ -584,13 +562,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 }) } @@ -627,13 +605,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 @@ -641,12 +619,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 }) } @@ -678,22 +656,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") @@ -705,7 +683,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 @@ -856,34 +834,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) @@ -893,8 +867,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 @@ -908,11 +882,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) } @@ -922,8 +896,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) @@ -935,11 +909,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) } @@ -948,8 +922,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) @@ -961,11 +935,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) } @@ -974,11 +948,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 } } @@ -997,7 +971,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) } @@ -1008,7 +982,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) @@ -1022,12 +996,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 () { @@ -1049,6 +1023,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 }) @@ -1062,22 +1052,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 cc91711..52fc1b4 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,72 +120,72 @@ 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 _has (key, options) { - return this[kParent].has(key, options) + return this.#parent.has(key, options) } async _hasMany (keys, options) { - return this[kParent].hasMany(keys, options) + return this.#parent.hasMany(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 203aaa4..866e130 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",