diff --git a/deferred-chained-batch.js b/deferred-chained-batch.js new file mode 100644 index 0000000..0bb41e8 --- /dev/null +++ b/deferred-chained-batch.js @@ -0,0 +1,28 @@ +'use strict' + +const { AbstractChainedBatch } = require('abstract-leveldown') +const kOperations = Symbol('operations') + +module.exports = class DeferredChainedBatch extends AbstractChainedBatch { + constructor (db) { + super(db) + this[kOperations] = [] + } + + _put (key, value, options) { + this[kOperations].push({ ...options, type: 'put', key, value }) + } + + _del (key, options) { + this[kOperations].push({ ...options, type: 'del', key }) + } + + _clear () { + this[kOperations] = [] + } + + _write (options, callback) { + // AbstractChainedBatch would call _batch(), we call batch() + this.db.batch(this[kOperations], options, callback) + } +} diff --git a/deferred-iterator.js b/deferred-iterator.js index 8e82204..de00973 100644 --- a/deferred-iterator.js +++ b/deferred-iterator.js @@ -1,40 +1,85 @@ 'use strict' -const AbstractIterator = require('abstract-leveldown').AbstractIterator +const { AbstractIterator } = require('abstract-leveldown') const inherits = require('inherits') +const getCallback = require('./util').getCallback + +const kOptions = Symbol('options') +const kIterator = Symbol('iterator') +const kOperations = Symbol('operations') +const kPromise = Symbol('promise') function DeferredIterator (db, options) { AbstractIterator.call(this, db) - this._options = options - this._iterator = null - this._operations = [] + this[kOptions] = options + this[kIterator] = null + this[kOperations] = [] } inherits(DeferredIterator, AbstractIterator) DeferredIterator.prototype.setDb = function (db) { - const it = this._iterator = db.iterator(this._options) + this[kIterator] = db.iterator(this[kOptions]) - for (const op of this._operations) { - it[op.method](...op.args) + for (const op of this[kOperations].splice(0, this[kOperations].length)) { + this[kIterator][op.method](...op.args) } } -DeferredIterator.prototype._operation = function (method, args) { - if (this._iterator) return this._iterator[method](...args) - this._operations.push({ method, args }) -} +DeferredIterator.prototype.next = function (...args) { + if (this.db.status === 'open') { + return this[kIterator].next(...args) + } + + const callback = getCallback(args, kPromise, function map (key, value) { + if (key === undefined && value === undefined) { + return undefined + } else { + return [key, value] + } + }) -for (const m of ['next', 'end']) { - DeferredIterator.prototype['_' + m] = function (...args) { - this._operation(m, args) + if (this.db.status === 'opening') { + this[kOperations].push({ method: 'next', args }) + } else { + this._nextTick(callback, new Error('Database is not open')) } + + return callback[kPromise] || this } -// Must defer seek() rather than _seek() because it requires db._serializeKey to be available DeferredIterator.prototype.seek = function (...args) { - this._operation('seek', args) + if (this.db.status === 'open') { + this[kIterator].seek(...args) + } else if (this.db.status === 'opening') { + this[kOperations].push({ method: 'seek', args }) + } else { + throw new Error('Database is not open') + } +} + +DeferredIterator.prototype.end = function (...args) { + if (this.db.status === 'open') { + return this[kIterator].end(...args) + } + + const callback = getCallback(args, kPromise) + + if (this.db.status === 'opening') { + this[kOperations].push({ method: 'end', args }) + } else { + this._nextTick(callback, new Error('Database is not open')) + } + + return callback[kPromise] || this +} + +for (const method of ['next', 'seek', 'end']) { + DeferredIterator.prototype['_' + method] = function () { + /* istanbul ignore next: assertion */ + throw new Error('Did not expect private method to be called: ' + method) + } } module.exports = DeferredIterator diff --git a/deferred-leveldown.js b/deferred-leveldown.js index fcea974..1720938 100644 --- a/deferred-leveldown.js +++ b/deferred-leveldown.js @@ -1,11 +1,18 @@ 'use strict' -const AbstractLevelDOWN = require('abstract-leveldown').AbstractLevelDOWN +const { AbstractLevelDOWN } = require('abstract-leveldown') const inherits = require('inherits') const DeferredIterator = require('./deferred-iterator') +const DeferredChainedBatch = require('./deferred-chained-batch') +const getCallback = require('./util').getCallback + const deferrables = ['put', 'get', 'getMany', 'del', 'batch', 'clear'] const optionalDeferrables = ['approximateSize', 'compactRange'] +const kInnerDb = Symbol('innerDb') +const kOperations = Symbol('operations') +const kPromise = Symbol('promise') + function DeferredLevelDOWN (db) { AbstractLevelDOWN.call(this, db.supports || {}) @@ -17,84 +24,109 @@ function DeferredLevelDOWN (db) { } } - this._db = db - this._operations = [] + this[kInnerDb] = db + this[kOperations] = [] - closed(this) + implement(this) } inherits(DeferredLevelDOWN, AbstractLevelDOWN) DeferredLevelDOWN.prototype.type = 'deferred-leveldown' +// Backwards compatibility for reachdown and subleveldown +Object.defineProperty(DeferredLevelDOWN.prototype, '_db', { + enumerable: true, + get () { + return this[kInnerDb] + } +}) + DeferredLevelDOWN.prototype._open = function (options, callback) { - this._db.open(options, (err) => { - if (err) return callback(err) + const onopen = (err) => { + if (err || this[kInnerDb].status !== 'open') { + // TODO: reject scheduled operations + return callback(err || new Error('Database is not open')) + } + + const operations = this[kOperations] + this[kOperations] = [] - for (const op of this._operations) { + for (const op of operations) { if (op.iterator) { - op.iterator.setDb(this._db) + op.iterator.setDb(this[kInnerDb]) } else { - this._db[op.method](...op.args) + this[kInnerDb][op.method](...op.args) } } - this._operations = [] + /* istanbul ignore if: assertion */ + if (this[kOperations].length > 0) { + throw new Error('Did not expect further operations') + } - open(this) callback() - }) + } + + if (this[kInnerDb].status === 'new' || this[kInnerDb].status === 'closed') { + this[kInnerDb].open(options, onopen) + } else { + this._nextTick(onopen) + } } DeferredLevelDOWN.prototype._close = function (callback) { - this._db.close((err) => { - if (err) return callback(err) - closed(this) - callback() - }) + this[kInnerDb].close(callback) } -function open (self) { - for (const m of deferrables.concat('iterator')) { - self['_' + m] = function (...args) { - return this._db[m](...args) - } - } - - for (const m of Object.keys(self.supports.additionalMethods)) { - self[m] = function (...args) { - return this._db[m](...args) - } - } +DeferredLevelDOWN.prototype._isOperational = function () { + return this.status === 'opening' } -function closed (self) { - for (const m of deferrables) { - self['_' + m] = function (...args) { - this._operations.push({ method: m, args }) +function implement (self) { + const additionalMethods = Object.keys(self.supports.additionalMethods) + + for (const method of deferrables.concat(additionalMethods)) { + // Override the public rather than private methods to cover cases where abstract-leveldown + // has a fast-path like on db.batch([]) which bypasses _batch() because the array is empty. + self[method] = function (...args) { + if (method === 'batch' && args.length === 0) { + return new DeferredChainedBatch(this) + } else if (this.status === 'open') { + return this[kInnerDb][method](...args) + } + + const callback = getCallback(args, kPromise) + + if (this.status === 'opening') { + this[kOperations].push({ method, args }) + } else { + this._nextTick(callback, new Error('Database is not open')) + } + + return callback[kPromise] } } - for (const m of Object.keys(self.supports.additionalMethods)) { - self[m] = function (...args) { - this._operations.push({ method: m, args }) + self.iterator = function (options) { + if (this.status === 'open') { + return this[kInnerDb].iterator(options) + } else if (this.status === 'opening') { + const iterator = new DeferredIterator(this, options) + this[kOperations].push({ iterator }) + return iterator + } else { + throw new Error('Database is not open') } } - self._iterator = function (options) { - const it = new DeferredIterator(self, options) - this._operations.push({ iterator: it }) - return it + for (const method of deferrables.concat(['iterator'])) { + self['_' + method] = function () { + /* istanbul ignore next: assertion */ + throw new Error('Did not expect private method to be called: ' + method) + } } } -DeferredLevelDOWN.prototype._serializeKey = function (key) { - return key -} - -DeferredLevelDOWN.prototype._serializeValue = function (value) { - return value -} - module.exports = DeferredLevelDOWN module.exports.DeferredIterator = DeferredIterator diff --git a/package.json b/package.json index 619a85d..c2c44e1 100644 --- a/package.json +++ b/package.json @@ -13,8 +13,10 @@ "prepublishOnly": "npm run dependency-check" }, "files": [ + "deferred-chained-batch.js", "deferred-iterator.js", "deferred-leveldown.js", + "util.js", "CHANGELOG.md", "LICENSE.md", "UPGRADING.md" diff --git a/test.js b/test.js index aab06cd..bb25769 100644 --- a/test.js +++ b/test.js @@ -24,20 +24,154 @@ const testCommon = suite.common({ getMany: true }) -// Hack: disable failing tests. These fail on serialize tests -require('abstract-leveldown/test/put-test').args = noop -require('abstract-leveldown/test/get-test').args = noop -require('abstract-leveldown/test/get-many-test').args = noop -require('abstract-leveldown/test/del-test').args = noop - -// This fails on "iterator has db reference" test, as expected because -// the return value of db.iterator() depends on whether the db is open. -require('abstract-leveldown/test/iterator-test').args = noop - // Test abstract-leveldown compliance suite(testCommon) // Custom tests +test('can open a new db', function (t) { + t.plan(8) + + const db = mockDown({ + _open: function (options, callback) { + t.pass('called') + this._nextTick(callback) + } + }) + + const ld = new DeferredLevelDOWN(db) + + t.is(db.status, 'new') + t.is(ld.status, 'new') + + ld.open(function (err) { + t.ifError(err) + t.is(db.status, 'open') + t.is(ld.status, 'open') + }) + + t.is(db.status, 'opening') + t.is(ld.status, 'opening') +}) + +test('can open an open db', function (t) { + t.plan(9) + + const db = mockDown({ + _open: function (options, callback) { + t.pass('called') + this._nextTick(callback) + } + }) + + db.open(function (err) { + t.ifError(err) + + const ld = new DeferredLevelDOWN(db) + + t.is(db.status, 'open') + t.is(ld.status, 'new') + + ld.open(function (err) { + t.ifError(err) + t.is(db.status, 'open') + t.is(ld.status, 'open') + }) + + t.is(db.status, 'open') + t.is(ld.status, 'opening') + }) +}) + +test('can open a closed db', function (t) { + let opens = 0 + + const db = mockDown({ + _open: function (options, callback) { + opens++ + this._nextTick(callback) + } + }) + + db.open(function (err) { + t.ifError(err) + t.is(opens, 1) + + db.close(function (err) { + t.ifError(err) + + const ld = new DeferredLevelDOWN(db) + + t.is(db.status, 'closed') + t.is(ld.status, 'new') + + ld.open(function (err) { + t.ifError(err) + t.is(opens, 2) + t.is(db.status, 'open') + t.is(ld.status, 'open') + t.end() + }) + }) + }) +}) + +test('cannot open a opening db', function (t) { + t.plan(7) + + const db = mockDown({ + _open: function (options, callback) { + t.pass('called') + this._nextTick(() => this._nextTick(callback)) + } + }) + + const ld = new DeferredLevelDOWN(db) + + db.open(function (err) { + t.ifError(err) + t.is(db.status, 'open') + }) + + ld.open(function (err) { + t.is(err && err.message, 'Database is not open') + t.is(ld.status, 'new') + }) + + t.is(db.status, 'opening') + t.is(ld.status, 'opening') +}) + +test('cannot open a closing db', function (t) { + t.plan(9) + + const db = mockDown({ + _close: function (callback) { + t.pass('called') + this._nextTick(() => this._nextTick(callback)) + } + }) + + const ld = new DeferredLevelDOWN(db) + + db.open(function (err) { + t.ifError(err) + t.is(db.status, 'open') + + db.close(function (err) { + t.ifError(err) + t.is(db.status, 'closed') + }) + + ld.open(function (err) { + t.is(err && err.message, 'Database is not open') + t.is(ld.status, 'new') + }) + + t.is(db.status, 'closing') + t.is(ld.status, 'opening') + }) +}) + test('deferred open gets correct options', function (t) { const OPTIONS = { foo: 'BAR' } const db = mockDown({ @@ -77,12 +211,12 @@ test('single operation', function (t) { const ld = new DeferredLevelDOWN(db) - ld.put('foo', 'bar', function (err) { + ld.open(function (err) { + t.is(called, true, 'called') t.error(err, 'no error') }) - ld.open(function (err) { - t.is(called, true, 'called') + ld.put('foo', 'bar', function (err) { t.error(err, 'no error') }) }) @@ -152,6 +286,30 @@ test('many operations', function (t) { let batches = 0 let clears = 0 + ld.open(function (err) { + t.error(err, 'no error') + t.ok(calls.length === 0, 'not called') + + // Wait a tick to account for async callbacks + // TODO: instead change the order of when we push into `calls` + ld._nextTick(function () { + t.equal(calls.length, 9, 'all functions called') + t.deepEqual(calls, [ + { type: 'put', key: 'foo1', v: 'put1' }, + { type: 'get', key: 'woo1', v: 'gets1' }, + { type: 'clear' }, + { type: 'put', key: 'foo2', v: 'put2' }, + { type: 'get', key: 'woo2', v: 'gets2' }, + { type: 'del', key: 'blergh', v: 'del' }, + { type: 'batch', keys: 'k1,k2' }, + { type: 'batch', keys: 'k3,k4' }, + { type: 'clear', gt: 'k5' } + ], 'calls correctly behaved') + + t.end() + }) + }) + ld.put('foo1', 'bar1', function (err, v) { t.error(err, 'no error') calls.push({ type: 'put', key: 'foo1', v: v }) @@ -193,29 +351,72 @@ test('many operations', function (t) { }) t.ok(calls.length === 0, 'not called') +}) + +test('cannot operate on new db', function (t) { + t.plan(2) + + const db = mockDown({}) + const ld = new DeferredLevelDOWN(db) + + ld.put('foo', 'bar', function (err) { + t.is(err && err.message, 'Database is not open') + }) + + try { + ld.iterator() + } catch (err) { + t.is(err.message, 'Database is not open') + } +}) + +test('cannot operate on closed db', function (t) { + t.plan(4) + + const db = mockDown({}) + const ld = new DeferredLevelDOWN(db) ld.open(function (err) { - t.error(err, 'no error') - t.ok(calls.length === 0, 'not called') + t.ifError(err) - // Wait a tick to account for async callbacks - // TODO: instead change the order of when we push into `calls` - ld._nextTick(function () { - t.equal(calls.length, 9, 'all functions called') - t.deepEqual(calls, [ - { type: 'put', key: 'foo1', v: 'put1' }, - { type: 'get', key: 'woo1', v: 'gets1' }, - { type: 'clear' }, - { type: 'put', key: 'foo2', v: 'put2' }, - { type: 'get', key: 'woo2', v: 'gets2' }, - { type: 'del', key: 'blergh', v: 'del' }, - { type: 'batch', keys: 'k1,k2' }, - { type: 'batch', keys: 'k3,k4' }, - { type: 'clear', gt: 'k5' } - ], 'calls correctly behaved') + ld.close(function (err) { + t.ifError(err) - t.end() + ld.put('foo', 'bar', function (err) { + t.is(err && err.message, 'Database is not open') + }) + + try { + ld.iterator() + } catch (err) { + t.is(err.message, 'Database is not open') + } + }) + }) +}) + +test('cannot operate on closing db', function (t) { + t.plan(4) + + const db = mockDown({}) + const ld = new DeferredLevelDOWN(db) + + ld.open(function (err) { + t.ifError(err) + + ld.close(function (err) { + t.ifError(err) }) + + ld.put('foo', 'bar', function (err) { + t.is(err && err.message, 'Database is not open') + }) + + try { + ld.iterator() + } catch (err) { + t.is(err.message, 'Database is not open') + } }) }) @@ -245,51 +446,50 @@ test('keys and values should not be serialized', function (t) { calls.push({ key: key, value: value }) } }) - DATA.forEach(function (d) { ld.put(d.key, d.value, noop) }) ld.open(function (err) { t.error(err, 'no error') t.same(calls, DATA, 'value ok') t.end() }) + DATA.forEach(function (d) { ld.put(d.key, d.value, noop) }) }) t.test('get', function (t) { const calls = [] const ld = Db({ _get: function (key) { calls.push(key) } }) - ITEMS.forEach(function (key) { ld.get(key, noop) }) ld.open(function (err) { t.error(err, 'no error') t.same(calls, ITEMS, 'value ok') t.end() }) + ITEMS.forEach(function (key) { ld.get(key, noop) }) }) t.test('getMany', function (t) { const calls = [] const ld = Db({ _getMany: function (keys) { calls.push(keys[0]) } }) - ITEMS.forEach(function (key) { ld.getMany([key], noop) }) ld.open(function (err) { t.error(err, 'no error') t.same(calls, ITEMS, 'value ok') t.end() }) + ITEMS.forEach(function (key) { ld.getMany([key], noop) }) }) t.test('del', function (t) { const calls = [] const ld = Db({ _del: function (key, cb) { calls.push(key) } }) - ITEMS.forEach(function (key) { ld.del(key, noop) }) ld.open(function (err) { t.error(err, 'no error') t.same(calls, ITEMS, 'value ok') t.end() }) + ITEMS.forEach(function (key) { ld.del(key, noop) }) }) t.test('clear', function (t) { const calls = [] const ld = Db({ _clear: function (opts, cb) { calls.push(opts) } }) - ITEMS.forEach(function (key) { ld.clear({ gt: key }, noop) }) ld.open(function (err) { t.error(err, 'no error') t.same(calls, ITEMS.map(function (key) { @@ -297,6 +497,7 @@ test('keys and values should not be serialized', function (t) { }), 'value ok') t.end() }) + ITEMS.forEach(function (key) { ld.clear({ gt: key }, noop) }) }) t.test('approximateSize', function (t) { @@ -306,7 +507,6 @@ test('keys and values should not be serialized', function (t) { calls.push({ start: start, end: end }) } }) - ITEMS.forEach(function (key) { ld.approximateSize(key, key, noop) }) ld.open(function (err) { t.error(err, 'no error') t.same(calls, ITEMS.map(function (i) { @@ -314,6 +514,7 @@ test('keys and values should not be serialized', function (t) { }), 'value ok') t.end() }) + ITEMS.forEach(function (key) { ld.approximateSize(key, key, noop) }) }) t.test('store not supporting approximateSize', function (t) { @@ -331,7 +532,6 @@ test('keys and values should not be serialized', function (t) { calls.push({ start: start, end: end }) } }) - ITEMS.forEach(function (key) { ld.compactRange(key, key, noop) }) ld.open(function (err) { t.error(err, 'no error') t.same(calls, ITEMS.map(function (i) { @@ -339,6 +539,7 @@ test('keys and values should not be serialized', function (t) { }), 'value ok') t.end() }) + ITEMS.forEach(function (key) { ld.compactRange(key, key, noop) }) }) t.test('store not supporting compactRange', function (t) { @@ -350,7 +551,7 @@ test('keys and values should not be serialized', function (t) { }) }) -test('_close calls close for underlying store', function (t) { +test('close calls close for underlying store', function (t) { t.plan(2) const db = mockDown({ @@ -421,8 +622,42 @@ test('clear() can schedule other operations itself', function (t) { const keys = ['foo'] const ld = new DeferredLevelDOWN(db) - ld.clear((err) => { t.error(err, 'no error') }) ld.open((err) => { t.error(err, 'no error') }) + ld.clear((err) => { t.error(err, 'no error') }) +}) + +test('chained batch serializes', function (t) { + t.plan(7) + + let called = false + + const db = mockDown({ + _batch: function (array, options, callback) { + called = true + t.is(array[0] && array[0].key, 'FOO') + this._nextTick(callback) + }, + _serializeKey (key) { + t.is(called, false, 'not yet called') + t.is(key, 'foo') + return key.toUpperCase() + }, + _open: function (options, callback) { + t.is(called, false, 'not yet called') + this._nextTick(callback) + } + }) + + const ld = new DeferredLevelDOWN(db) + + ld.open(function (err) { + t.is(called, true, 'called') + t.error(err, 'no error') + }) + + ld.batch().put('foo', 'bar').write(function (err) { + t.error(err, 'no error') + }) }) test('non-deferred approximateSize', function (t) { @@ -471,13 +706,15 @@ test('non-deferred compactRange', function (t) { }) }) -test('iterator - deferred operations', function (t) { - t.plan(9) +test('deferred iterator', function (t) { + t.plan(11) let seekTarget = false const db = mockDown({ _iterator: function (options) { + t.is(options.gt, 'FOO') + return mockIterator(this, { _seek: function (target) { seekTarget = target @@ -490,18 +727,29 @@ test('iterator - deferred operations', function (t) { } }) }, + _serializeKey: function (key) { + t.is(key, 'foo') + return key.toUpperCase() + }, _open: function (options, callback) { this._nextTick(callback) } }) const ld = new DeferredLevelDOWN(db) - const it = ld.iterator() + + ld.open(function (err) { + t.error(err, 'no error') + }) + + const it = ld.iterator({ gt: 'foo' }) + t.ok(it instanceof DeferredLevelDOWN.DeferredIterator) + let nextFirst = false it.seek('foo') it.next(function (err, key, value) { - t.is(seekTarget, 'foo', 'seek was called with correct target') + t.is(seekTarget, 'FOO', 'seek was called with correct target') nextFirst = true t.error(err, 'no error') t.equal(key, 'key') @@ -512,18 +760,10 @@ test('iterator - deferred operations', function (t) { t.error(err, 'no error') t.ok(nextFirst) }) - - ld.open(function (err) { - t.error(err, 'no error') - const it2 = ld.iterator() - it2.end(t.error.bind(t)) - }) - - t.ok(require('./').DeferredIterator) }) -test('iterator - non deferred operation', function (t) { - t.plan(5) +test('non-deferred iterator', function (t) { + t.plan(6) let seekTarget = false const db = mockDown({ @@ -545,13 +785,14 @@ test('iterator - non deferred operation', function (t) { } }) const ld = new DeferredLevelDOWN(db) - const it = ld.iterator() ld.open(function (err) { t.error(err, 'no error') - it.seek('foo') + const it = ld.iterator() + t.notOk(it instanceof DeferredLevelDOWN.DeferredIterator) + it.seek('foo') t.is(seekTarget, 'foo', 'seek was called with correct target') it.next(function (err, key, value) { @@ -562,18 +803,149 @@ test('iterator - non deferred operation', function (t) { }) }) +test('deferred iterator - non-deferred operations', function (t) { + t.plan(7) + + const ld = new DeferredLevelDOWN(mockDown({ + _iterator: function (options) { + return mockIterator(this, { + _seek (target) { + t.is(target, 'foo') + }, + _next (cb) { + this._nextTick(cb, null, 'key', 'value') + } + }) + } + })) + + ld.open(function (err) { + t.error(err, 'no error') + + it.seek('foo') + it.next(function (err, key, value) { + t.error(err, 'no error') + t.equal(key, 'key') + t.equal(value, 'value') + + it.end(function (err) { + t.error(err, 'no error') + }) + }) + }) + + const it = ld.iterator({ gt: 'foo' }) + t.ok(it instanceof DeferredLevelDOWN.DeferredIterator) +}) + +test('deferred iterator - cannot operate on closed db', function (t) { + t.plan(8) + + const ld = new DeferredLevelDOWN(mockDown({ + _iterator: function (options) { + return mockIterator(this, { + _next: function (cb) { + t.fail('should not be called') + } + }) + } + })) + + ld.open(function (err) { + t.error(err, 'no error') + + ld.close(function (err) { + t.ifError(err) + + it.next(function (err, key, value) { + t.is(err && err.message, 'Database is not open') + }) + + it.next().catch(function (err) { + t.is(err.message, 'Database is not open') + }) + + it.end(function (err) { + t.is(err && err.message, 'Database is not open') + }) + + it.end().catch(function (err) { + t.is(err.message, 'Database is not open') + }) + + try { + it.seek('foo') + } catch (err) { + t.is(err.message, 'Database is not open') + } + }) + }) + + const it = ld.iterator({ gt: 'foo' }) + t.ok(it instanceof DeferredLevelDOWN.DeferredIterator) +}) + +test('deferred iterator - cannot operate on closing db', function (t) { + t.plan(8) + + const ld = new DeferredLevelDOWN(mockDown({ + _iterator: function (options) { + return mockIterator(this, { + _next: function (cb) { + t.fail('should not be called') + } + }) + } + })) + + ld.open(function (err) { + t.error(err, 'no error') + + ld.close(function (err) { + t.ifError(err) + }) + + it.next(function (err, key, value) { + t.is(err && err.message, 'Database is not open') + }) + + it.next().catch(function (err) { + t.is(err.message, 'Database is not open') + }) + + it.end(function (err) { + t.is(err && err.message, 'Database is not open') + }) + + it.end().catch(function (err) { + t.is(err.message, 'Database is not open') + }) + + try { + it.seek('foo') + } catch (err) { + t.is(err.message, 'Database is not open') + } + }) + + const it = ld.iterator({ gt: 'foo' }) + t.ok(it instanceof DeferredLevelDOWN.DeferredIterator) +}) + test('iterator - is created in order', function (t) { t.plan(4) - function db () { + const order1 = [] + const order2 = [] + + function db (order) { return mockDown({ - order: [], _iterator: function (options) { - this.order.push('iterator created') + order.push('iterator created') return mockIterator(this, {}) }, _put: function (key, value, options, callback) { - this.order.push('put') + order.push('put') }, _open: function (options, callback) { this._nextTick(callback) @@ -581,29 +953,56 @@ test('iterator - is created in order', function (t) { }) } - const ld1 = new DeferredLevelDOWN(db()) - const ld2 = new DeferredLevelDOWN(db()) - - ld1.iterator() - ld1.put('key', 'value', noop) - - ld2.put('key', 'value', noop) - ld2.iterator() + const ld1 = new DeferredLevelDOWN(db(order1)) + const ld2 = new DeferredLevelDOWN(db(order2)) ld1.open(function (err) { t.error(err, 'no error') - t.same(ld1._db.order, ['iterator created', 'put']) + t.same(order1, ['iterator created', 'put']) }) ld2.open(function (err) { t.error(err, 'no error') - t.same(ld2._db.order, ['put', 'iterator created']) + t.same(order2, ['put', 'iterator created']) }) + + ld1.iterator() + ld1.put('key', 'value', noop) + + ld2.put('key', 'value', noop) + ld2.iterator() +}) + +test('for await...of iterator', async function (t) { + const db = new DeferredLevelDOWN(memdown()) + const entries = [] + + db.open(t.ifError.bind(t)) + db.batch().put('a', '1').put('b', '2').write(t.ifError.bind(t)) + + for await (const kv of db.iterator({ keyAsBuffer: false, valueAsBuffer: false })) { + entries.push(kv) + } + + t.same(entries, [['a', '1'], ['b', '2']]) +}) + +test('for await...of iterator (empty)', async function (t) { + const db = new DeferredLevelDOWN(memdown()) + const entries = [] + + db.open(t.ifError.bind(t)) + + for await (const kv of db.iterator({ keyAsBuffer: false, valueAsBuffer: false })) { + entries.push(kv) + } + + t.same(entries, []) }) test('reachdown supports deferred-leveldown', function (t) { // Define just enough methods for reachdown to see this as a real db - const db = { open: noop, _batch: noop, _iterator: noop } + const db = { status: 'new', open: noop, _batch: noop, _iterator: noop } const ld = new DeferredLevelDOWN(db) t.is(ld.type, 'deferred-leveldown') diff --git a/util.js b/util.js new file mode 100644 index 0000000..eeee7ff --- /dev/null +++ b/util.js @@ -0,0 +1,18 @@ +'use strict' + +exports.getCallback = function (args, symbol, map) { + let callback = args[args.length - 1] + + if (typeof callback !== 'function') { + const promise = new Promise((resolve, reject) => { + args.push(callback = function (err, ...results) { + if (err) reject(err) + else resolve(map ? map(...results) : results[0]) + }) + }) + + callback[symbol] = promise + } + + return callback +}