Skip to content

Add db.keys(), .values(), iterator.nextv() and .all() #12

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jan 16, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
324 changes: 269 additions & 55 deletions README.md

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions UPGRADING.md
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,8 @@ super({

Node.js readable streams must now be created with a new standalone module called [`level-read-stream`](https://github.com/Level/read-stream), rather than database methods like `db.createReadStream()`. Please see its [upgrade guide](https://github.com/Level/read-stream/blob/main/UPGRADING.md#100) for details.

To offer an alternative to `db.createKeyStream()` and `db.createValueStream()`, two new types of iterators have been added: `db.keys()` and `db.values()`. Their default implementations are functional but implementors may want to override them for optimal performance. The same goes for two new methods on iterators: `nextv()` and `all()`. To achieve this and honor the `limit` option, abstract iterators now count how many items they yielded, which may remove the need for implementations to do so on their own. Please see the README for details.

### 4. Zero-length keys and range options are now valid

These keys sort before anything else. Historically they weren't supported for causing segmentation faults in `leveldown`. That doesn't apply to today's codebase. Implementations must now support:
Expand Down Expand Up @@ -408,8 +410,10 @@ The abstract test suite of `abstract-level` has some breaking changes compared t

- Options to skip tests have been removed in favor of `db.supports`
- Support of `db.clear()` and `db.getMany()` is now mandatory. The default (slow) implementation of `_clear()` has been removed.
- Added tests that `gte` and `lte` range options take precedence over `gt` and `lt` respectively. This is incompatible with [`ltgt`](https://github.com/dominictarr/ltgt) but aligns with `subleveldown`, [`level-option-wrap`](https://github.com/substack/level-option-wrap) and half of `leveldown`. There was no good choice.
- The `setUp` and `tearDown` functions have been removed from the test suite and `suite.common()`.
- Added ability to access manifests via `testCommon.supports`, by lazily copying it from `testCommon.factory().supports`. This requires that the manifest does not change during the lifetime of a `db`.
- Your `factory()` function must now accept an `options` argument.

Many tests were imported from `levelup`, `encoding-down`, `deferred-leveldown`, `memdown`, `level-js` and `leveldown`. They test the changes described above and improve coverage of existing behavior.

Expand Down
255 changes: 127 additions & 128 deletions abstract-chained-batch.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,172 +10,171 @@ const kOperations = Symbol('operations')
const kFinishClose = Symbol('finishClose')
const kCloseCallbacks = Symbol('closeCallbacks')

function AbstractChainedBatch (db) {
if (typeof db !== 'object' || db === null) {
const hint = db === null ? 'null' : typeof db
throw new TypeError(`The first argument must be an abstract-level database, received ${hint}`)
}

this[kOperations] = []
this[kCloseCallbacks] = []
this[kStatus] = 'open'
this[kFinishClose] = this[kFinishClose].bind(this)
class AbstractChainedBatch {
constructor (db) {
if (typeof db !== 'object' || db === null) {
const hint = db === null ? 'null' : typeof db
throw new TypeError(`The first argument must be an abstract-level database, received ${hint}`)
}

this.db = db
this.db.attachResource(this)
this.nextTick = db.nextTick
}
this[kOperations] = []
this[kCloseCallbacks] = []
this[kStatus] = 'open'
this[kFinishClose] = this[kFinishClose].bind(this)

Object.defineProperty(AbstractChainedBatch.prototype, 'length', {
enumerable: true,
get () {
return this[kOperations].length
this.db = db
this.db.attachResource(this)
this.nextTick = db.nextTick
}
})

AbstractChainedBatch.prototype.put = function (key, value, options) {
if (this[kStatus] !== 'open') {
throw new ModuleError('Batch is not open: cannot call put() after write() or close()', {
code: 'LEVEL_BATCH_NOT_OPEN'
})
get length () {
return this[kOperations].length
}

const err = this.db._checkKey(key) || this.db._checkValue(value)
if (err) throw err
put (key, value, options) {
if (this[kStatus] !== 'open') {
throw new ModuleError('Batch is not open: cannot call put() after write() or close()', {
code: 'LEVEL_BATCH_NOT_OPEN'
})
}

const db = options && options.sublevel != null ? options.sublevel : this.db
const original = options
const keyEncoding = db.keyEncoding(options && options.keyEncoding)
const valueEncoding = db.valueEncoding(options && options.valueEncoding)
const keyFormat = keyEncoding.format
const err = this.db._checkKey(key) || this.db._checkValue(value)
if (err) throw err

// Forward encoding options
options = { ...options, keyEncoding: keyFormat, valueEncoding: valueEncoding.format }
const db = options && options.sublevel != null ? options.sublevel : this.db
const original = options
const keyEncoding = db.keyEncoding(options && options.keyEncoding)
const valueEncoding = db.valueEncoding(options && options.valueEncoding)
const keyFormat = keyEncoding.format

// Prevent double prefixing
if (db !== this.db) {
options.sublevel = null
}
// Forward encoding options
options = { ...options, keyEncoding: keyFormat, valueEncoding: valueEncoding.format }

const mappedKey = db.prefixKey(keyEncoding.encode(key), keyFormat)
const mappedValue = valueEncoding.encode(value)

this._put(mappedKey, mappedValue, options)
this[kOperations].push({ ...original, type: 'put', key, value })
// Prevent double prefixing
if (db !== this.db) {
options.sublevel = null
}

return this
}
const mappedKey = db.prefixKey(keyEncoding.encode(key), keyFormat)
const mappedValue = valueEncoding.encode(value)

AbstractChainedBatch.prototype._put = function (key, value, options) {}
this._put(mappedKey, mappedValue, options)
this[kOperations].push({ ...original, type: 'put', key, value })

AbstractChainedBatch.prototype.del = function (key, options) {
if (this[kStatus] !== 'open') {
throw new ModuleError('Batch is not open: cannot call del() after write() or close()', {
code: 'LEVEL_BATCH_NOT_OPEN'
})
return this
}

const err = this.db._checkKey(key)
if (err) throw err
_put (key, value, options) {}

const db = options && options.sublevel != null ? options.sublevel : this.db
const original = options
const keyEncoding = db.keyEncoding(options && options.keyEncoding)
const keyFormat = keyEncoding.format
del (key, options) {
if (this[kStatus] !== 'open') {
throw new ModuleError('Batch is not open: cannot call del() after write() or close()', {
code: 'LEVEL_BATCH_NOT_OPEN'
})
}

// Forward encoding options
options = { ...options, keyEncoding: keyFormat }
const err = this.db._checkKey(key)
if (err) throw err

// Prevent double prefixing
if (db !== this.db) {
options.sublevel = null
}
const db = options && options.sublevel != null ? options.sublevel : this.db
const original = options
const keyEncoding = db.keyEncoding(options && options.keyEncoding)
const keyFormat = keyEncoding.format

this._del(db.prefixKey(keyEncoding.encode(key), keyFormat), options)
this[kOperations].push({ ...original, type: 'del', key })
// Forward encoding options
options = { ...options, keyEncoding: keyFormat }

return this
}
// Prevent double prefixing
if (db !== this.db) {
options.sublevel = null
}

AbstractChainedBatch.prototype._del = function (key, options) {}
this._del(db.prefixKey(keyEncoding.encode(key), keyFormat), options)
this[kOperations].push({ ...original, type: 'del', key })

AbstractChainedBatch.prototype.clear = function () {
if (this[kStatus] !== 'open') {
throw new ModuleError('Batch is not open: cannot call clear() after write() or close()', {
code: 'LEVEL_BATCH_NOT_OPEN'
})
return this
}

this._clear()
this[kOperations] = []
_del (key, options) {}

return this
}
clear () {
if (this[kStatus] !== 'open') {
throw new ModuleError('Batch is not open: cannot call clear() after write() or close()', {
code: 'LEVEL_BATCH_NOT_OPEN'
})
}

AbstractChainedBatch.prototype._clear = function () {}

AbstractChainedBatch.prototype.write = function (options, callback) {
callback = getCallback(options, callback)
callback = fromCallback(callback, kPromise)
options = getOptions(options)

if (this[kStatus] !== 'open') {
this.nextTick(callback, new ModuleError('Batch is not open: cannot call write() after write() or close()', {
code: 'LEVEL_BATCH_NOT_OPEN'
}))
} else if (this.length === 0) {
this.close(callback)
} else {
this[kStatus] = 'writing'
this._write(options, (err) => {
this[kStatus] = 'closing'
this[kCloseCallbacks].push(() => callback(err))

// Emit after setting 'closing' status, because event may trigger a
// db close which in turn triggers (idempotently) closing this batch.
if (!err) this.db.emit('batch', this[kOperations])

this._close(this[kFinishClose])
})
this._clear()
this[kOperations] = []

return this
}

return callback[kPromise]
}
_clear () {}

write (options, callback) {
callback = getCallback(options, callback)
callback = fromCallback(callback, kPromise)
options = getOptions(options)

if (this[kStatus] !== 'open') {
this.nextTick(callback, new ModuleError('Batch is not open: cannot call write() after write() or close()', {
code: 'LEVEL_BATCH_NOT_OPEN'
}))
} else if (this.length === 0) {
this.close(callback)
} else {
this[kStatus] = 'writing'
this._write(options, (err) => {
this[kStatus] = 'closing'
this[kCloseCallbacks].push(() => callback(err))

// Emit after setting 'closing' status, because event may trigger a
// db close which in turn triggers (idempotently) closing this batch.
if (!err) this.db.emit('batch', this[kOperations])

this._close(this[kFinishClose])
})
}

return callback[kPromise]
}

AbstractChainedBatch.prototype._write = function (options, callback) {}
_write (options, callback) {}

AbstractChainedBatch.prototype.close = function (callback) {
callback = fromCallback(callback, kPromise)
close (callback) {
callback = fromCallback(callback, kPromise)

if (this[kStatus] === 'closing') {
this[kCloseCallbacks].push(callback)
} else if (this[kStatus] === 'closed') {
this.nextTick(callback)
} else {
this[kCloseCallbacks].push(callback)
if (this[kStatus] === 'closing') {
this[kCloseCallbacks].push(callback)
} else if (this[kStatus] === 'closed') {
this.nextTick(callback)
} else {
this[kCloseCallbacks].push(callback)

if (this[kStatus] !== 'writing') {
this[kStatus] = 'closing'
this._close(this[kFinishClose])
if (this[kStatus] !== 'writing') {
this[kStatus] = 'closing'
this._close(this[kFinishClose])
}
}
}

return callback[kPromise]
}
return callback[kPromise]
}

AbstractChainedBatch.prototype._close = function (callback) {
this.nextTick(callback)
}
_close (callback) {
this.nextTick(callback)
}

AbstractChainedBatch.prototype[kFinishClose] = function () {
this[kStatus] = 'closed'
this.db.detachResource(this)
[kFinishClose] () {
this[kStatus] = 'closed'
this.db.detachResource(this)

const callbacks = this[kCloseCallbacks]
this[kCloseCallbacks] = []
const callbacks = this[kCloseCallbacks]
this[kCloseCallbacks] = []

for (const cb of callbacks) {
cb()
for (const cb of callbacks) {
cb()
}
}
}

Expand Down
Loading