Skip to content

Commit

Permalink
Breaking: parent db must support deferredOpen
Browse files Browse the repository at this point in the history
DeferredOpen means that the db opens itself and defers operations
until it's open. Currently that's only supported by levelup (and
levelup factories like level). Previously, subleveldown would also
accept abstract-leveldown db's that were not wrapped in levelup.

Opening and closing a sublevel no longer opens or closes the parent
db. The sublevel does wait for the parent to open (which in the
case of levelup already happens automatically) but never initiates
a state change.

If one closes the parent but not the sublevel, subsequent
operations (like get and put) on the sublevel will yield an error,
to prevent segmentation faults from underlying stores.

Drops support of old modules:

- memdb (use level-mem instead)
- deferred-leveldown < 2.0.0 (and thus levelup < 2.0.0)
- abstract-leveldown < 2.4.0

Closes #84, #83 and #60.
  • Loading branch information
vweevers committed Apr 5, 2020
1 parent 01b2d50 commit 966f5d6
Show file tree
Hide file tree
Showing 4 changed files with 266 additions and 76 deletions.
90 changes: 72 additions & 18 deletions leveldown.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ function SubIterator (db, ite, prefix) {
inherits(SubIterator, abstract.AbstractIterator)

SubIterator.prototype._next = function (cb) {
if (maybeError(this.db.leveldown, cb)) return

var self = this
this.iterator.next(function (err, key, value) {
if (err) return cb(err)
Expand All @@ -39,6 +41,7 @@ SubIterator.prototype._seek = function (key) {
}

SubIterator.prototype._end = function (cb) {
if (maybeError(this.db.leveldown, cb)) return
this.iterator.end(cb)
}

Expand All @@ -64,12 +67,39 @@ function SubDown (db, prefix, opts) {
})

this.db = db
this.leveldown = null
this.ownPrefix = separator + prefix + separator
this.prefix = this.ownPrefix
this.prefix = separator + prefix + separator
this._beforeOpen = opts.open

var self = this
var manifest = db.supports || {}

// The parent db must open itself or be (re)opened by the user because a
// sublevel can't (shouldn't) initiate state changes on the rest of the db.
if (!manifest.deferredOpen && !reachdown.is(db, 'levelup')) {
throw new Error('Parent database must support deferredOpen')
}

var subdb = reachdown(db, 'subleveldown')

if (subdb) {
// Old subleveldown doesn't know its prefix and leveldown until opened
if (!subdb.prefix || !subdb.leveldown) {
throw new Error('Incompatible with subleveldown < 5.0.0')
}

this.prefix = subdb.prefix + this.prefix
this.leveldown = subdb.leveldown
} else {
this.leveldown = reachdown(db, matchdown, false)
}

if (reachdown.is(this.leveldown, 'deferred-leveldown')) {
// Old deferred-leveldown doesn't expose its underlying db until opened
throw new Error('Incompatible with deferred-leveldown < 2.0.0')
} else if (!this.leveldown.status) {
// Old abstract-leveldown doesn't have a status property
throw new Error('Incompatible with abstract-leveldown < 2.4.0')
}

this._wrap = {
gt: function (x) {
Expand All @@ -91,47 +121,55 @@ inherits(SubDown, abstract.AbstractLevelDOWN)

SubDown.prototype.type = 'subleveldown'

// TODO: remove _open() once abstract-leveldown supports deferredOpen,
// because that means we can always do operations on this.leveldown.
// Alternatively have the sublevel follow the open state of this.db.
SubDown.prototype._open = function (opts, cb) {
var self = this

this.db.open(function (err) {
if (err) return cb(err)
// TODO: make _isOpening public in levelup or add a method like
// ready(cb) which waits for - but does not initiate - a state change.
var m = typeof this.db.isOpening === 'function' ? 'isOpening' : '_isOpening'

var subdb = reachdown(self.db, 'subleveldown')
if (this.db[m]()) {
this.db.once('open', onopen)
} else {
this._nextTick(onopen)
}

if (subdb && subdb.prefix) {
self.prefix = subdb.prefix + self.ownPrefix
self.leveldown = subdb.leveldown
} else {
self.leveldown = reachdown(self.db, matchdown, false)
}
function onopen () {
if (!self.db.isOpen()) return cb(new Error('Parent database is not open'))
if (self.leveldown.status !== 'open') return cb(new Error('Inner database is not open'))

if (self._beforeOpen) self._beforeOpen(cb)
else cb()
})
}
// TODO: add hooks to abstract-leveldown
if (self._beforeOpen) return self._beforeOpen(cb)

SubDown.prototype._close = function (cb) {
this.leveldown.close(cb)
cb()
}
}

SubDown.prototype._serializeKey = function (key) {
return Buffer.isBuffer(key) ? key : String(key)
}

SubDown.prototype._put = function (key, value, opts, cb) {
if (maybeError(this.leveldown, cb)) return
this.leveldown.put(concat(this.prefix, key), value, opts, cb)
}

SubDown.prototype._get = function (key, opts, cb) {
if (maybeError(this.leveldown, cb)) return
this.leveldown.get(concat(this.prefix, key), opts, cb)
}

SubDown.prototype._del = function (key, opts, cb) {
if (maybeError(this.leveldown, cb)) return
this.leveldown.del(concat(this.prefix, key), opts, cb)
}

SubDown.prototype._batch = function (operations, opts, cb) {
if (maybeError(this.leveldown, cb)) return

// No need to make a copy of the array, abstract-leveldown does that
for (var i = 0; i < operations.length; i++) {
operations[i].key = concat(this.prefix, operations[i].key)
Expand All @@ -141,6 +179,8 @@ SubDown.prototype._batch = function (operations, opts, cb) {
}

SubDown.prototype._clear = function (opts, cb) {
if (maybeError(this.leveldown, cb)) return

if (typeof this.leveldown.clear === 'function') {
// Prefer optimized implementation of clear()
opts = addRestOptions(wrap(opts, this._wrap), opts)
Expand Down Expand Up @@ -169,6 +209,20 @@ function isEmptyBuffer (key) {
return Buffer.isBuffer(key) && key.length === 0
}

// Before any operation, check if the inner db is open. Needed
// because we don't follow open state of the parent db atm.
// TODO: move to abstract-leveldown
function maybeError (leveldown, callback) {
if (leveldown.status !== 'open') {
// Same error message as levelup
// TODO: use require('level-errors').ReadError
process.nextTick(callback, new Error('Database is not open'))
return true
}

return false
}

// TODO (refactor): use addRestOptions instead
function extend (xopts, opts) {
xopts.keys = opts.keys
Expand Down
1 change: 1 addition & 0 deletions matchdown.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module.exports = function matchdown (db, type) {
if (type === 'levelup') return false
if (type === 'encoding-down') return false
if (type === 'deferred-leveldown') return false
if (type === 'subleveldown') return false

return true
}
5 changes: 2 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@
"test": "test"
},
"dependencies": {
"abstract-leveldown": "^6.1.1",
"abstract-leveldown": "^6.2.3",
"encoding-down": "^6.2.0",
"inherits": "^2.0.3",
"level-option-wrap": "^1.1.0",
"levelup": "^4.3.1",
"reachdown": "^1.0.0"
"reachdown": "^1.1.0"
},
"devDependencies": {
"after": "^0.8.2",
Expand All @@ -31,7 +31,6 @@
"hallmark": "^2.0.0",
"level-community": "^3.0.0",
"level-concat-iterator": "^2.0.1",
"memdb": "^1.3.1",
"memdown": "^5.0.0",
"nyc": "^14.0.0",
"standard": "^14.0.0",
Expand Down
Loading

0 comments on commit 966f5d6

Please sign in to comment.