diff --git a/binding.cc b/binding.cc index 789fce8..2c35932 100644 --- a/binding.cc +++ b/binding.cc @@ -139,6 +139,22 @@ static bool BooleanProperty (napi_env env, napi_value obj, const char* key, return DEFAULT; } +/** + * Returns true if the options object contains an encoding option that is "buffer" + */ +static bool EncodingIsBuffer (napi_env env, napi_value options, const char* option) { + napi_value value; + size_t size; + + if (napi_get_named_property(env, options, option, &value) == napi_ok && + napi_get_value_string_utf8(env, value, NULL, 0, &size) == napi_ok) { + // Value is either "buffer" or "utf8" so we can tell them apart just by size + return size == 6; + } + + return false; +} + /** * Returns a uint32 property 'key' from 'obj'. * Returns 'DEFAULT' if the property doesn't exist. @@ -291,37 +307,30 @@ enum Mode { * Helper struct for caching and converting a key-value pair to napi_values. */ struct Entry { - Entry (const leveldb::Slice* key, const leveldb::Slice* value) { - key_ = key != NULL ? new std::string(key->data(), key->size()) : NULL; - value_ = value != NULL ? new std::string(value->data(), value->size()) : NULL; - } - - ~Entry () { - if (key_ != NULL) delete key_; - if (value_ != NULL) delete value_; - } + Entry (const leveldb::Slice* key, const leveldb::Slice* value) + : key_(key->data(), key->size()), + value_(value->data(), value->size()) {} - // Not used yet. - void ConvertXX (napi_env env, Mode mode, bool keyAsBuffer, bool valueAsBuffer, napi_value* result) { + void ConvertByMode (napi_env env, Mode mode, const bool keyAsBuffer, const bool valueAsBuffer, napi_value* result) { if (mode == Mode::entries) { napi_create_array_with_length(env, 2, result); - napi_value valueElement; napi_value keyElement; + napi_value valueElement; - Convert(env, key_, keyAsBuffer, &keyElement); - Convert(env, value_, valueAsBuffer, &valueElement); + Convert(env, &key_, keyAsBuffer, &keyElement); + Convert(env, &value_, valueAsBuffer, &valueElement); napi_set_element(env, *result, 0, keyElement); napi_set_element(env, *result, 1, valueElement); } else if (mode == Mode::keys) { - Convert(env, key_, keyAsBuffer, result); + Convert(env, &key_, keyAsBuffer, result); } else { - Convert(env, value_, valueAsBuffer, result); + Convert(env, &value_, valueAsBuffer, result); } } - static void Convert (napi_env env, const std::string* s, bool asBuffer, napi_value* result) { + static void Convert (napi_env env, const std::string* s, const bool asBuffer, napi_value* result) { if (s == NULL) { napi_get_undefined(env, result); } else if (asBuffer) { @@ -332,8 +341,8 @@ struct Entry { } private: - std::string* key_; - std::string* value_; + std::string key_; + std::string value_; }; /** @@ -690,6 +699,7 @@ struct BaseIterator { } } + // TODO: rename to Close() void End () { if (!hasEnded_) { hasEnded_ = true; @@ -824,34 +834,36 @@ struct Iterator final : public BaseIterator { bool ReadMany (uint32_t size) { cache_.clear(); + cache_.reserve(size); size_t bytesRead = 0; + leveldb::Slice empty; while (true) { if (landed_) Next(); if (!Valid() || !Increment()) break; - if (keys_) { - leveldb::Slice slice = CurrentKey(); - cache_.emplace_back(slice.data(), slice.size()); - bytesRead += slice.size(); - } else { - cache_.emplace_back(""); - } - - if (values_) { - leveldb::Slice slice = CurrentValue(); - cache_.emplace_back(slice.data(), slice.size()); - bytesRead += slice.size(); - } else { - cache_.emplace_back(""); + if (keys_ && values_) { + leveldb::Slice k = CurrentKey(); + leveldb::Slice v = CurrentValue(); + cache_.emplace_back(&k, &v); + bytesRead += k.size() + v.size(); + } else if (keys_) { + leveldb::Slice k = CurrentKey(); + cache_.emplace_back(&k, &empty); + bytesRead += k.size(); + } else if (values_) { + leveldb::Slice v = CurrentValue(); + cache_.emplace_back(&empty, &v); + bytesRead += v.size(); } + // TODO: this logic should only apply to next(). Can it be moved to JS? if (!landed_) { landed_ = true; return true; } - if (bytesRead > highWaterMark_ || cache_.size() >= size * 2) { + if (bytesRead > highWaterMark_ || cache_.size() >= size) { return true; } } @@ -869,7 +881,7 @@ struct Iterator final : public BaseIterator { bool nexting_; bool isEnding_; BaseWorker* endWorker_; - std::vector cache_; + std::vector cache_; private: napi_ref ref_; @@ -1055,6 +1067,7 @@ NAPI_METHOD(db_close) { std::map::iterator it; for (it = iterators.begin(); it != iterators.end(); ++it) { + // TODO: rename to iterator_close_do iterator_end_do(env, it->second, noop); } @@ -1155,7 +1168,7 @@ NAPI_METHOD(db_get) { leveldb::Slice key = ToSlice(env, argv[1]); napi_value options = argv[2]; - const bool asBuffer = BooleanProperty(env, options, "asBuffer", true); + const bool asBuffer = EncodingIsBuffer(env, options, "valueEncoding"); const bool fillCache = BooleanProperty(env, options, "fillCache", true); napi_value callback = argv[3]; @@ -1246,7 +1259,7 @@ NAPI_METHOD(db_get_many) { const std::vector* keys = KeyArray(env, argv[1]); napi_value options = argv[2]; - const bool asBuffer = BooleanProperty(env, options, "asBuffer", true); + const bool asBuffer = EncodingIsBuffer(env, options, "valueEncoding"); const bool fillCache = BooleanProperty(env, options, "fillCache", true); napi_value callback = argv[3]; @@ -1595,8 +1608,8 @@ NAPI_METHOD(iterator_init) { const bool keys = BooleanProperty(env, options, "keys", true); const bool values = BooleanProperty(env, options, "values", true); const bool fillCache = BooleanProperty(env, options, "fillCache", false); - const bool keyAsBuffer = BooleanProperty(env, options, "keyAsBuffer", true); - const bool valueAsBuffer = BooleanProperty(env, options, "valueAsBuffer", true); + const bool keyAsBuffer = EncodingIsBuffer(env, options, "keyEncoding"); + const bool valueAsBuffer = EncodingIsBuffer(env, options, "valueEncoding"); const int limit = Int32Property(env, options, "limit", -1); const uint32_t highWaterMark = Uint32Property(env, options, "highWaterMark", 16 * 1024); @@ -1644,6 +1657,7 @@ NAPI_METHOD(iterator_seek) { /** * Worker class for ending an iterator + * TODO: rename to CloseIteratorWorker */ struct EndWorker final : public BaseWorker { EndWorker (napi_env env, @@ -1677,6 +1691,7 @@ static void iterator_end_do (napi_env env, Iterator* iterator, napi_value cb) { iterator->isEnding_ = true; if (iterator->nexting_) { + // TODO: rename to closeWorker_ iterator->endWorker_ = worker; } else { worker->Queue(env); @@ -1702,10 +1717,11 @@ NAPI_METHOD(iterator_end) { struct NextWorker final : public BaseWorker { NextWorker (napi_env env, Iterator* iterator, + uint32_t size, napi_value callback) : BaseWorker(env, iterator->database_, callback, "classic_level.iterator.next"), - iterator_(iterator), ok_() {} + iterator_(iterator), size_(size), ok_() {} ~NextWorker () {} @@ -1714,9 +1730,7 @@ struct NextWorker final : public BaseWorker { iterator_->SeekToRange(); } - // Limit the size of the cache to prevent starving the event loop - // in JS-land while we're recursively calling process.nextTick(). - ok_ = iterator_->ReadMany(1000); + ok_ = iterator_->ReadMany(size_); if (!ok_) { SetStatus(iterator_->Status()); @@ -1724,23 +1738,17 @@ struct NextWorker final : public BaseWorker { } void HandleOKCallback (napi_env env, napi_value callback) override { - size_t arraySize = iterator_->cache_.size(); + size_t size = iterator_->cache_.size(); napi_value jsArray; - napi_create_array_with_length(env, arraySize, &jsArray); + napi_create_array_with_length(env, size, &jsArray); - for (size_t idx = 0; idx < iterator_->cache_.size(); idx += 2) { - std::string key = iterator_->cache_[idx]; - std::string value = iterator_->cache_[idx + 1]; + const bool kab = iterator_->keyAsBuffer_; + const bool vab = iterator_->valueAsBuffer_; - napi_value returnKey; - napi_value returnValue; - - Entry::Convert(env, &key, iterator_->keyAsBuffer_, &returnKey); - Entry::Convert(env, &value, iterator_->valueAsBuffer_, &returnValue); - - // put the key & value in a descending order, so that they can be .pop:ed in javascript-land - napi_set_element(env, jsArray, static_cast(arraySize - idx - 1), returnKey); - napi_set_element(env, jsArray, static_cast(arraySize - idx - 2), returnValue); + for (uint32_t idx = 0; idx < size; idx++) { + napi_value element; + iterator_->cache_[idx].ConvertByMode(env, Mode::entries, kab, vab, &element); + napi_set_element(env, jsArray, idx, element); } napi_value argv[3]; @@ -1764,6 +1772,7 @@ struct NextWorker final : public BaseWorker { private: Iterator* iterator_; + uint32_t size_; bool ok_; }; @@ -1771,19 +1780,23 @@ struct NextWorker final : public BaseWorker { * Moves an iterator to next element. */ NAPI_METHOD(iterator_next) { - NAPI_ARGV(2); + NAPI_ARGV(3); NAPI_ITERATOR_CONTEXT(); - napi_value callback = argv[1]; + uint32_t size; + NAPI_STATUS_THROWS(napi_get_value_uint32(env, argv[1], &size)); + if (size == 0) size = 1; - if (iterator->isEnding_ || iterator->hasEnded_) { - napi_value argv = CreateError(env, "iterator has ended"); - CallFunction(env, callback, 1, &argv); + napi_value callback = argv[2]; + // TODO: rename to isClosing/hasClosed + if (iterator->isEnding_ || iterator->hasEnded_) { + napi_value argv = CreateCodeError(env, "LEVEL_ITERATOR_NOT_OPEN", "Iterator is not open"); + NAPI_STATUS_THROWS(CallFunction(env, callback, 1, &argv)); NAPI_RETURN_UNDEFINED(); } - NextWorker* worker = new NextWorker(env, iterator, callback); + NextWorker* worker = new NextWorker(env, iterator, size, callback); iterator->nexting_ = true; worker->Queue(env); diff --git a/chained-batch.js b/chained-batch.js index ab7a743..0491a93 100644 --- a/chained-batch.js +++ b/chained-batch.js @@ -1,30 +1,38 @@ 'use strict' -const util = require('util') -const AbstractChainedBatch = require('abstract-leveldown').AbstractChainedBatch +const { AbstractChainedBatch } = require('abstract-level') const binding = require('./binding') -function ChainedBatch (db) { - AbstractChainedBatch.call(this, db) - this.context = binding.batch_init(db.context) +const kContext = Symbol('context') + +class ChainedBatch extends AbstractChainedBatch { + constructor (db, context) { + super(db) + this[kContext] = binding.batch_init(context) + } } +// TODO: move to class + ChainedBatch.prototype._put = function (key, value) { - binding.batch_put(this.context, key, value) + binding.batch_put(this[kContext], key, value) } ChainedBatch.prototype._del = function (key) { - binding.batch_del(this.context, key) + binding.batch_del(this[kContext], key) } ChainedBatch.prototype._clear = function () { - binding.batch_clear(this.context) + binding.batch_clear(this[kContext]) } ChainedBatch.prototype._write = function (options, callback) { - binding.batch_write(this.context, options, callback) + binding.batch_write(this[kContext], options, callback) } -util.inherits(ChainedBatch, AbstractChainedBatch) +ChainedBatch.prototype._close = function (callback) { + // TODO: close native batch (currently done on GC) + process.nextTick(callback) +} -module.exports = ChainedBatch +exports.ChainedBatch = ChainedBatch diff --git a/iterator.js b/iterator.js index 2805c33..e1432aa 100644 --- a/iterator.js +++ b/iterator.js @@ -1,50 +1,93 @@ 'use strict' -const util = require('util') -const AbstractIterator = require('abstract-leveldown').AbstractIterator +const { AbstractIterator } = require('abstract-level') const binding = require('./binding') -function Iterator (db, options) { - AbstractIterator.call(this, db) +const kContext = Symbol('context') +const kCache = Symbol('cache') +const kFinished = Symbol('finished') +const kPosition = Symbol('position') +const kHandleNext = Symbol('handleNext') +const kHandleNextv = Symbol('handleNextv') +const kCallback = Symbol('callback') +const empty = [] - this.context = binding.iterator_init(db.context, options) - this.cache = null - this.finished = false -} +class Iterator extends AbstractIterator { + constructor (db, context, options) { + super(db, options) + + this[kContext] = binding.iterator_init(context, options) + this[kHandleNext] = this[kHandleNext].bind(this) + this[kHandleNextv] = this[kHandleNextv].bind(this) + this[kCallback] = null + this[kCache] = empty + this[kFinished] = false + this[kPosition] = 0 + } -util.inherits(Iterator, AbstractIterator) + _seek (target, options) { + this[kCache] = empty + this[kFinished] = false + this[kPosition] = 0 -Iterator.prototype._seek = function (target) { - if (target.length === 0) { - throw new Error('cannot seek() to an empty target') + binding.iterator_seek(this[kContext], target) } - this.cache = null - binding.iterator_seek(this.context, target) - this.finished = false -} + // TODO: rename iterator_next + _next (callback) { + if (this[kPosition] < this[kCache].length) { + const entry = this[kCache][this[kPosition]++] + process.nextTick(callback, null, entry[0], entry[1]) + } else if (this[kFinished]) { + process.nextTick(callback) + } else { + this[kCallback] = callback -Iterator.prototype._next = function (callback) { - if (this.cache && this.cache.length) { - process.nextTick(callback, null, this.cache.pop(), this.cache.pop()) - } else if (this.finished) { - process.nextTick(callback) - } else { - binding.iterator_next(this.context, (err, array, finished) => { - if (err) return callback(err) + // Limit the size of the cache to prevent starving the event loop + // while we're recursively calling process.nextTick(). + binding.iterator_next(this[kContext], 1000, this[kHandleNext]) + } + } + + [kHandleNext] (err, items, finished) { + const callback = this[kCallback] + if (err) return callback(err) - this.cache = array - this.finished = finished - this._next(callback) - }) + this[kCache] = items + this[kFinished] = finished + this[kPosition] = 0 + + this._next(callback) } - return this -} + _nextv (size, options, callback) { + if (this[kFinished]) { + process.nextTick(callback, null, []) + } else { + this[kCallback] = callback + binding.iterator_next(this[kContext], size, this[kHandleNextv]) + } + } + + [kHandleNextv] (err, items, finished) { + const callback = this[kCallback] + if (err) return callback(err) + this[kFinished] = finished + callback(null, items) + } + + _close (callback) { + this[kCache] = empty + this[kCallback] = null -Iterator.prototype._end = function (callback) { - delete this.cache - binding.iterator_end(this.context, callback) + // TODO: rename to iterator_close + binding.iterator_end(this[kContext], callback) + } + + // Undocumented, exposed for tests only + get cached () { + return this[kCache].length - this[kPosition] + } } -module.exports = Iterator +exports.Iterator = Iterator diff --git a/leveldown.js b/leveldown.js index 11b2dfc..230ce8c 100644 --- a/leveldown.js +++ b/leveldown.js @@ -1,164 +1,185 @@ 'use strict' -const util = require('util') -const AbstractLevelDOWN = require('abstract-leveldown').AbstractLevelDOWN -const binding = require('./binding') -const ChainedBatch = require('./chained-batch') -const Iterator = require('./iterator') - -function LevelDOWN (location) { - if (!(this instanceof LevelDOWN)) { - return new LevelDOWN(location) - } +// TODO: rename file - if (typeof location !== 'string') { - throw new Error('constructor requires a location string argument') - } - - AbstractLevelDOWN.call(this, { - bufferKeys: true, - snapshots: true, - permanence: true, - seek: true, - clear: true, - getMany: true, - createIfMissing: true, - errorIfExists: true, - additionalMethods: { - approximateSize: true, - compactRange: true +const { AbstractLevel } = require('abstract-level') +const ModuleError = require('module-error') +const { fromCallback } = require('catering') +const binding = require('./binding') +const { ChainedBatch } = require('./chained-batch') +const { Iterator } = require('./iterator') + +const kPromise = Symbol('promise') +const kContext = Symbol('context') +const kLocation = Symbol('location') + +class ClassicLevel extends AbstractLevel { + constructor (location, options, _) { + // To help migrating to abstract-level + if (typeof options === 'function' || typeof _ === 'function') { + throw new ModuleError('The levelup-style callback argument has been removed', { + code: 'LEVEL_LEGACY' + }) } - }) - this.location = location - this.context = binding.db_init() -} + if (typeof location !== 'string' || location === '') { + throw new TypeError("The first argument 'location' must be a non-empty string") + } -util.inherits(LevelDOWN, AbstractLevelDOWN) + super({ + encodings: { + buffer: true, + utf8: true + }, + seek: true, + createIfMissing: true, + errorIfExists: true, + additionalMethods: { + approximateSize: true, + compactRange: true + } + }, options) + + this[kLocation] = location + this[kContext] = binding.db_init() + } -LevelDOWN.prototype._open = function (options, callback) { - binding.db_open(this.context, this.location, options, callback) + get location () { + return this[kLocation] + } } -LevelDOWN.prototype._close = function (callback) { - binding.db_close(this.context, callback) -} +// TODO: move to class -LevelDOWN.prototype._serializeKey = function (key) { - return Buffer.isBuffer(key) ? key : String(key) +ClassicLevel.prototype._open = function (options, callback) { + binding.db_open(this[kContext], this[kLocation], options, callback) } -LevelDOWN.prototype._serializeValue = function (value) { - return Buffer.isBuffer(value) ? value : String(value) +ClassicLevel.prototype._close = function (callback) { + binding.db_close(this[kContext], callback) } -LevelDOWN.prototype._put = function (key, value, options, callback) { - binding.db_put(this.context, key, value, options, callback) +ClassicLevel.prototype._put = function (key, value, options, callback) { + binding.db_put(this[kContext], key, value, options, callback) } -LevelDOWN.prototype._get = function (key, options, callback) { - binding.db_get(this.context, key, options, callback) +ClassicLevel.prototype._get = function (key, options, callback) { + binding.db_get(this[kContext], key, options, callback) } -LevelDOWN.prototype._getMany = function (keys, options, callback) { - binding.db_get_many(this.context, keys, options, callback) +ClassicLevel.prototype._getMany = function (keys, options, callback) { + binding.db_get_many(this[kContext], keys, options, callback) } -LevelDOWN.prototype._del = function (key, options, callback) { - binding.db_del(this.context, key, options, callback) +ClassicLevel.prototype._del = function (key, options, callback) { + binding.db_del(this[kContext], key, options, callback) } -LevelDOWN.prototype._clear = function (options, callback) { - binding.db_clear(this.context, options, callback) +ClassicLevel.prototype._clear = function (options, callback) { + binding.db_clear(this[kContext], options, callback) } -LevelDOWN.prototype._chainedBatch = function () { - return new ChainedBatch(this) +ClassicLevel.prototype._chainedBatch = function () { + return new ChainedBatch(this, this[kContext]) } -LevelDOWN.prototype._batch = function (operations, options, callback) { - binding.batch_do(this.context, operations, options, callback) +ClassicLevel.prototype._batch = function (operations, options, callback) { + binding.batch_do(this[kContext], operations, options, callback) } -LevelDOWN.prototype.approximateSize = function (start, end, callback) { - if (start == null || - end == null || - typeof start === 'function' || - typeof end === 'function') { - throw new Error('approximateSize() requires valid `start` and `end` arguments') +ClassicLevel.prototype.approximateSize = function (start, end, options, callback) { + if (arguments.length < 2 || typeof start === 'function' || typeof end === 'function') { + throw new TypeError("The arguments 'start' and 'end' are required") + } else if (typeof options === 'function') { + callback = options + options = null + } else if (typeof options !== 'object') { + options = null } - if (typeof callback !== 'function') { - throw new Error('approximateSize() requires a callback argument') + callback = fromCallback(callback, kPromise) + + if (this.status === 'opening') { + this.defer(() => this.approximateSize(start, end, options, callback)) + } else if (this.status !== 'open') { + this.nextTick(callback, new ModuleError('Database is not open: cannot call approximateSize()', { + code: 'LEVEL_DATABASE_NOT_OPEN' + })) + } else { + const keyEncoding = this.keyEncoding(options && options.keyEncoding) + start = keyEncoding.encode(start) + end = keyEncoding.encode(end) + binding.db_approximate_size(this[kContext], start, end, callback) } - start = this._serializeKey(start) - end = this._serializeKey(end) - - binding.db_approximate_size(this.context, start, end, callback) + return callback[kPromise] } -LevelDOWN.prototype.compactRange = function (start, end, callback) { - if (start == null || - end == null || - typeof start === 'function' || - typeof end === 'function') { - throw new Error('compactRange() requires valid `start` and `end` arguments') +ClassicLevel.prototype.compactRange = function (start, end, options, callback) { + if (arguments.length < 2 || typeof start === 'function' || typeof end === 'function') { + throw new TypeError("The arguments 'start' and 'end' are required") + } else if (typeof options === 'function') { + callback = options + options = null + } else if (typeof options !== 'object') { + options = null } - if (typeof callback !== 'function') { - throw new Error('compactRange() requires a callback argument') + callback = fromCallback(callback, kPromise) + + if (this.status === 'opening') { + this.defer(() => this.compactRange(start, end, options, callback)) + } else if (this.status !== 'open') { + this.nextTick(callback, new ModuleError('Database is not open: cannot call compactRange()', { + code: 'LEVEL_DATABASE_NOT_OPEN' + })) + } else { + const keyEncoding = this.keyEncoding(options && options.keyEncoding) + start = keyEncoding.encode(start) + end = keyEncoding.encode(end) + binding.db_compact_range(this[kContext], start, end, callback) } - start = this._serializeKey(start) - end = this._serializeKey(end) - - binding.db_compact_range(this.context, start, end, callback) + return callback[kPromise] } -LevelDOWN.prototype.getProperty = function (property) { +ClassicLevel.prototype.getProperty = function (property) { if (typeof property !== 'string') { - throw new Error('getProperty() requires a valid `property` argument') + throw new TypeError("The first argument 'property' must be a string") } - return binding.db_get_property(this.context, property) -} - -LevelDOWN.prototype._iterator = function (options) { + // Is synchronous, so can't be deferred if (this.status !== 'open') { - // Prevent segfault - throw new Error('cannot call iterator() before open()') + throw new ModuleError('Database is not open', { + code: 'LEVEL_DATABASE_NOT_OPEN' + }) } - return new Iterator(this, options) + return binding.db_get_property(this[kContext], property) } -LevelDOWN.destroy = function (location, callback) { - if (arguments.length < 2) { - throw new Error('destroy() requires `location` and `callback` arguments') - } - if (typeof location !== 'string') { - throw new Error('destroy() requires a location string argument') - } - if (typeof callback !== 'function') { - throw new Error('destroy() requires a callback function argument') +ClassicLevel.prototype._iterator = function (options) { + return new Iterator(this, this[kContext], options) +} + +ClassicLevel.destroy = function (location, callback) { + if (typeof location !== 'string' || location === '') { + throw new TypeError("The first argument 'location' must be a non-empty string") } + callback = fromCallback(callback, kPromise) binding.destroy_db(location, callback) + return callback[kPromise] } -LevelDOWN.repair = function (location, callback) { - if (arguments.length < 2) { - throw new Error('repair() requires `location` and `callback` arguments') - } - if (typeof location !== 'string') { - throw new Error('repair() requires a location string argument') - } - if (typeof callback !== 'function') { - throw new Error('repair() requires a callback function argument') +ClassicLevel.repair = function (location, callback) { + if (typeof location !== 'string' || location === '') { + throw new TypeError("The first argument 'location' must be a non-empty string") } + callback = fromCallback(callback, kPromise) binding.repair_db(location, callback) + return callback[kPromise] } -module.exports = LevelDOWN +exports.ClassicLevel = ClassicLevel diff --git a/package.json b/package.json index 65ee3ba..79a7c13 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,9 @@ "prebuild-win32-x64": "prebuildify -t 8.14.0 --napi --strip" }, "dependencies": { - "abstract-leveldown": "^7.2.0", + "abstract-level": "^1.0.0", + "catering": "^2.1.0", + "module-error": "^1.0.1", "napi-macros": "~2.0.0", "node-gyp-build": "^4.3.0" }, @@ -39,7 +41,6 @@ "faucet": "^0.0.1", "glob": "^7.1.3", "hallmark": "^3.0.0", - "level-concat-iterator": "^3.0.0", "mkfiletree": "^2.0.0", "node-gyp": "^7.1.2", "nyc": "^15.0.0", diff --git a/test/abstract-level-test.js b/test/abstract-level-test.js new file mode 100644 index 0000000..0b43203 --- /dev/null +++ b/test/abstract-level-test.js @@ -0,0 +1,2 @@ +// Test abstract-level compliance +require('abstract-level/test')(require('./common')) diff --git a/test/abstract-leveldown-test.js b/test/abstract-leveldown-test.js deleted file mode 100644 index 12888ea..0000000 --- a/test/abstract-leveldown-test.js +++ /dev/null @@ -1 +0,0 @@ -require('abstract-leveldown/test')(require('./common')) diff --git a/test/approximate-size-test.js b/test/approximate-size-test.js index 189d6fe..a4e242e 100644 --- a/test/approximate-size-test.js +++ b/test/approximate-size-test.js @@ -2,85 +2,23 @@ const test = require('tape') const testCommon = require('./common') +const noop = () => {} let db -test('setUp common for approximate size', testCommon.setUp) - test('setUp db', function (t) { db = testCommon.factory() db.open(t.end.bind(t)) }) -test('test argument-less approximateSize() throws', function (t) { - t.throws( - db.approximateSize.bind(db) - , /^Error: approximateSize\(\) requires valid `start` and `end` arguments/ - , 'no-arg approximateSize() throws' - ) - t.end() -}) - -test('test callback-less, 1-arg, approximateSize() throws', function (t) { - t.throws( - db.approximateSize.bind(db, 'foo') - , /^Error: approximateSize\(\) requires valid `start` and `end` arguments/ - , 'callback-less, 1-arg approximateSize() throws' - ) - t.end() -}) - -test('test callback-less, 2-arg, approximateSize() throws', function (t) { - t.throws( - db.approximateSize.bind(db, 'foo', 'bar') - , /^Error: approximateSize\(\) requires a callback argument/ - , 'callback-less, 2-arg approximateSize() throws' - ) - t.end() -}) - -test('test callback-less, 3-arg, approximateSize() throws', function (t) { - t.throws( - db.approximateSize.bind(db, function () {}) - , /^Error: approximateSize\(\) requires valid `start` and `end` arguments/ - , 'callback-only approximateSize() throws' - ) - t.end() -}) - -test('test callback-only approximateSize() throws', function (t) { - t.throws( - db.approximateSize.bind(db, function () {}) - , /^Error: approximateSize\(\) requires valid `start` and `end` arguments/ - , 'callback-only approximateSize() throws' - ) - t.end() -}) - -test('test 1-arg + callback approximateSize() throws', function (t) { - t.throws( - db.approximateSize.bind(db, 'foo', function () {}) - , /^Error: approximateSize\(\) requires valid `start` and `end` arguments/ - , '1-arg + callback approximateSize() throws' - ) - t.end() -}) - -test('test custom _serialize*', function (t) { - t.plan(4) - const db = testCommon.factory() - db._serializeKey = function (data) { return data } - db.approximateSize = function (start, end, callback) { - t.deepEqual(start, { foo: 'bar' }) - t.deepEqual(end, { beep: 'boop' }) - process.nextTick(callback) - } - db.open(function () { - db.approximateSize({ foo: 'bar' }, { beep: 'boop' }, function (err) { - t.error(err) - db.close(t.error.bind(t)) +test('test approximateSize() throws if arguments are missing', function (t) { + for (const args of [[], ['foo'], [noop], ['foo', noop]]) { + t.throws(() => db.approximateSize(...args), { + name: 'TypeError', + message: "The arguments 'start' and 'end' are required" }) - }) + } + t.end() }) test('test approximateSize()', function (t) { @@ -115,5 +53,58 @@ test('test approximateSize()', function (t) { }) test('tearDown', function (t) { - db.close(testCommon.tearDown.bind(null, t)) + db.close(t.end.bind(t)) +}) + +test('test approximateSize() yields error if db is closed', function (t) { + db.approximateSize('foo', 'foo', function (err) { + t.is(err && err.code, 'LEVEL_DATABASE_NOT_OPEN') + t.end() + }) +}) + +test('test approximateSize() is deferred', async function (t) { + const opening = db.open().then(() => 'opening') + const deferred = db.approximateSize('a', 'b').then(() => 'deferred') + t.is(await Promise.race([opening, deferred]), 'opening') + t.same(await Promise.all([opening, deferred]), ['opening', 'deferred']) + return db.close() +}) + +// NOTE: adapted from encoding-down +test('encodes start and end of approximateSize()', async function (t) { + const calls = [] + const keyEncoding = { + name: 'test', + format: 'utf8', + encode (key) { + calls.push(key) + return key + }, + decode: (v) => v + } + const db = testCommon.factory({ keyEncoding }) + await db.open() + await db.approximateSize('a', 'b') + t.same(calls, ['a', 'b']) + return db.close() +}) + +// NOTE: adapted from encoding-down +test('encodes start and end of approximateSize() with custom encoding', async function (t) { + const calls = [] + const keyEncoding = { + name: 'test', + format: 'utf8', + encode (key) { + calls.push(key) + return key + }, + decode: (v) => v + } + const db = testCommon.factory() + await db.open() + await db.approximateSize('a', 'b', { keyEncoding }) + t.same(calls, ['a', 'b']) + return db.close() }) diff --git a/test/cleanup-hanging-iterators-test.js b/test/cleanup-hanging-iterators-test.js index ffc783f..0e81ac3 100644 --- a/test/cleanup-hanging-iterators-test.js +++ b/test/cleanup-hanging-iterators-test.js @@ -3,25 +3,25 @@ const makeTest = require('./make') const repeats = 200 -makeTest('test ended iterator', function (db, t, done) { - // First test normal and proper usage: calling it.end() before db.close() - const it = db.iterator({ keyAsBuffer: false, valueAsBuffer: false }) +makeTest('test closed iterator', function (db, t, done) { + // First test normal and proper usage: calling it.close() before db.close() + const it = db.iterator() it.next(function (err, key, value) { t.ifError(err, 'no error from next()') t.equal(key, 'one', 'correct key') t.equal(value, '1', 'correct value') - it.end(function (err) { - t.ifError(err, 'no error from end()') + it.close(function (err) { + t.ifError(err, 'no error from close()') done() }) }) }) -makeTest('test likely-ended iterator', function (db, t, done) { - // Test improper usage: not calling it.end() before db.close(). Cleanup of the +makeTest('test likely-closed iterator', function (db, t, done) { + // Test improper usage: not calling it.close() before db.close(). Cleanup of the // database will crash Node if not handled properly. - const it = db.iterator({ keyAsBuffer: false, valueAsBuffer: false }) + const it = db.iterator() it.next(function (err, key, value) { t.ifError(err, 'no error from next()') @@ -31,15 +31,11 @@ makeTest('test likely-ended iterator', function (db, t, done) { }) }) -makeTest('test non-ended iterator', function (db, t, done) { +makeTest('test non-closed iterator', function (db, t, done) { // Same as the test above but with a highWaterMark of 0 so that we don't // preemptively fetch all records, to ensure that the iterator is still // active when we (attempt to) close the database. - const it = db.iterator({ - highWaterMark: 0, - keyAsBuffer: false, - valueAsBuffer: false - }) + const it = db.iterator({ highWaterMark: 0 }) it.next(function (err, key, value) { t.ifError(err, 'no error from next()') @@ -49,7 +45,7 @@ makeTest('test non-ended iterator', function (db, t, done) { }) }) -makeTest('test multiple likely-ended iterators', function (db, t, done) { +makeTest('test multiple likely-closed iterators', function (db, t, done) { // Same as the test above but repeated and with an extra iterator that is not // nexting, which means its EndWorker will be executed almost immediately. for (let i = 0; i < repeats; i++) { @@ -60,7 +56,7 @@ makeTest('test multiple likely-ended iterators', function (db, t, done) { setTimeout(done, Math.floor(Math.random() * 50)) }) -makeTest('test multiple non-ended iterators', function (db, t, done) { +makeTest('test multiple non-closed iterators', function (db, t, done) { // Same as the test above but with a highWaterMark of 0. for (let i = 0; i < repeats; i++) { db.iterator({ highWaterMark: 0 }) @@ -70,9 +66,9 @@ makeTest('test multiple non-ended iterators', function (db, t, done) { setTimeout(done, Math.floor(Math.random() * 50)) }) -global.gc && makeTest('test multiple non-ended iterators with forced gc', function (db, t, done) { +global.gc && makeTest('test multiple non-closed iterators with forced gc', function (db, t, done) { // Same as the test above but with forced GC, to test that the lifespan of an - // iterator is tied to *both* its JS object and whether the iterator was ended. + // iterator is tied to *both* its JS object and whether the iterator was closed. for (let i = 0; i < repeats; i++) { db.iterator({ highWaterMark: 0 }) db.iterator({ highWaterMark: 0 }).next(function () {}) @@ -84,13 +80,15 @@ global.gc && makeTest('test multiple non-ended iterators with forced gc', functi }, Math.floor(Math.random() * 50)) }) -makeTest('test ending iterators', function (db, t, done) { +makeTest('test closing iterators', function (db, t, done) { // At least one end() should be in progress when we try to close the db. - const it1 = db.iterator().next(function () { - it1.end(function () {}) + const it1 = db.iterator() + it1.next(function () { + it1.close(function () {}) }) - const it2 = db.iterator().next(function () { - it2.end(function () {}) + const it2 = db.iterator() + it2.next(function () { + it2.close(function () {}) done() }) }) @@ -100,7 +98,7 @@ makeTest('test recursive next', function (db, t, done) { const it = db.iterator({ highWaterMark: 0 }) it.next(function loop (err, key) { - if (err && err.message !== 'iterator has ended') throw err + if (err && err.code !== 'LEVEL_ITERATOR_NOT_OPEN') throw err if (key !== undefined) it.next(loop) }) @@ -112,7 +110,7 @@ makeTest('test recursive next (random)', function (db, t, done) { const it = db.iterator({ highWaterMark: 0 }) it.next(function loop (err, key) { - if (err && err.message !== 'iterator has ended') throw err + if (err && err.code !== 'LEVEL_ITERATOR_NOT_OPEN') throw err if (key !== undefined) it.next(loop) }) diff --git a/test/common.js b/test/common.js index 881d901..be1e2c8 100644 --- a/test/common.js +++ b/test/common.js @@ -2,16 +2,12 @@ const test = require('tape') const tempy = require('tempy') -const leveldown = require('..') -const suite = require('abstract-leveldown/test') +const { ClassicLevel } = require('..') +const suite = require('abstract-level/test') module.exports = suite.common({ - test: test, - factory: function () { - return leveldown(tempy.directory()) - }, - - // Opt-in to new tests - clear: true, - getMany: true + test, + factory (options) { + return new ClassicLevel(tempy.directory(), options) + } }) diff --git a/test/compact-range-test.js b/test/compact-range-test.js index 3687dcc..7de0fe0 100644 --- a/test/compact-range-test.js +++ b/test/compact-range-test.js @@ -2,16 +2,25 @@ const test = require('tape') const testCommon = require('./common') +const noop = () => {} let db -test('setUp common', testCommon.setUp) - test('setUp db', function (t) { db = testCommon.factory() db.open(t.end.bind(t)) }) +test('test compactRange() throws if arguments are missing', function (t) { + for (const args of [[], ['foo'], [noop], ['foo', noop]]) { + t.throws(() => db.compactRange(...args), { + name: 'TypeError', + message: "The arguments 'start' and 'end' are required" + }) + } + t.end() +}) + test('test compactRange() frees disk space after key deletion', function (t) { const key1 = '000000' const key2 = '000001' @@ -45,22 +54,59 @@ test('test compactRange() frees disk space after key deletion', function (t) { }) }) -test('test compactRange() serializes start and end', function (t) { - t.plan(3) +test('tearDown', function (t) { + db.close(t.end.bind(t)) +}) - const clone = Object.create(db) - let count = 0 +test('test compactRange() yields error if db is closed', function (t) { + db.compactRange('foo', 'foo', function (err) { + t.is(err && err.code, 'LEVEL_DATABASE_NOT_OPEN') + t.end() + }) +}) - clone._serializeKey = function (key) { - t.is(key, count++) - return db._serializeKey(key) - } +test('test compactRange() is deferred', async function (t) { + const opening = db.open().then(() => 'opening') + const deferred = db.compactRange('a', 'b').then(() => 'deferred') + t.is(await Promise.race([opening, deferred]), 'opening') + t.same(await Promise.all([opening, deferred]), ['opening', 'deferred']) + return db.close() +}) - clone.compactRange(0, 1, function (err) { - t.ifError(err, 'no compactRange error') - }) +// NOTE: copied from encoding-down +test('encodes start and end of compactRange()', async function (t) { + const calls = [] + const keyEncoding = { + name: 'test', + format: 'utf8', + encode (key) { + calls.push(key) + return key + }, + decode: (v) => v + } + const db = testCommon.factory({ keyEncoding }) + await db.open() + await db.compactRange('a', 'b') + t.same(calls, ['a', 'b']) + return db.close() }) -test('tearDown', function (t) { - db.close(testCommon.tearDown.bind(null, t)) +// NOTE: adapted from encoding-down +test('encodes start and end of compactRange() with custom encoding', async function (t) { + const calls = [] + const keyEncoding = { + name: 'test', + format: 'utf8', + encode (key) { + calls.push(key) + return key + }, + decode: (v) => v + } + const db = testCommon.factory() + await db.open() + await db.compactRange('a', 'b', { keyEncoding }) + t.same(calls, ['a', 'b']) + return db.close() }) diff --git a/test/compression-test.js b/test/compression-test.js index cc21e45..da1f73c 100644 --- a/test/compression-test.js +++ b/test/compression-test.js @@ -4,7 +4,7 @@ const each = require('async-each') const du = require('du') const delayed = require('delayed') const testCommon = require('./common') -const leveldown = require('..') +const { ClassicLevel } = require('..') const test = require('tape') const compressableData = Buffer.from(Array.apply(null, Array(1024 * 100)).map(function () { @@ -31,8 +31,8 @@ const cycle = function (db, compression, t, callback) { const location = db.location db.close(function (err) { t.error(err) - db = leveldown(location) - db.open({ errorIfExists: false, compression: compression }, function () { + db = new ClassicLevel(location) + db.open({ errorIfExists: false, compression }, function () { t.error(err) db.close(function (err) { t.error(err) @@ -43,8 +43,7 @@ const cycle = function (db, compression, t, callback) { } test('compression', function (t) { - t.plan(4) - t.test('set up', testCommon.setUp) + t.plan(3) t.test('test data is compressed by default (db.put())', function (t) { const db = testCommon.factory() diff --git a/test/destroy-test.js b/test/destroy-test.js index 509be9c..eff3413 100644 --- a/test/destroy-test.js +++ b/test/destroy-test.js @@ -7,22 +7,18 @@ const path = require('path') const mkfiletree = require('mkfiletree') const readfiletree = require('readfiletree') const rimraf = require('rimraf') -const leveldown = require('..') +const { ClassicLevel } = require('..') const makeTest = require('./make') -test('test argument-less destroy() throws', function (t) { - t.throws(leveldown.destroy, { - name: 'Error', - message: 'destroy() requires `location` and `callback` arguments' - }, 'no-arg destroy() throws') - t.end() -}) - -test('test callback-less, 1-arg, destroy() throws', function (t) { - t.throws(leveldown.destroy.bind(null, 'foo'), { - name: 'Error', - message: 'destroy() requires `location` and `callback` arguments' - }, 'callback-less, 1-arg destroy() throws') +test('test destroy() without location throws', function (t) { + t.throws(ClassicLevel.destroy, { + name: 'TypeError', + message: "The first argument 'location' must be a non-empty string" + }) + t.throws(() => ClassicLevel.destroy(''), { + name: 'TypeError', + message: "The first argument 'location' must be a non-empty string" + }) t.end() }) @@ -39,7 +35,7 @@ test('test destroy non-existent directory', function (t) { rimraf(location, { glob: false }, function (err) { t.ifError(err, 'no error from rimraf()') - leveldown.destroy(location, function (err) { + ClassicLevel.destroy(location, function (err) { t.error(err, 'no error') // Assert that destroy() didn't inadvertently create the directory. @@ -57,7 +53,7 @@ test('test destroy non-existent parent directory', function (t) { t.notOk(fs.existsSync(parent), 'parent does not exist before') - leveldown.destroy(location, function (err) { + ClassicLevel.destroy(location, function (err) { t.error(err, 'no error') t.notOk(fs.existsSync(location), 'directory does not exist after') }) @@ -72,7 +68,7 @@ test('test destroy non leveldb directory', function (t) { mkfiletree.makeTemp('destroy-test', tree, function (err, dir) { t.ifError(err, 'no error from makeTemp()') - leveldown.destroy(dir, function (err) { + ClassicLevel.destroy(dir, function (err) { t.ifError(err, 'no error from destroy()') readfiletree(dir, function (err, actual) { @@ -93,7 +89,7 @@ makeTest('test destroy() cleans and removes leveldb-only dir', function (db, t, db.close(function (err) { t.ifError(err, 'no error from close()') - leveldown.destroy(location, function (err) { + ClassicLevel.destroy(location, function (err) { t.ifError(err, 'no error from destroy()') t.notOk(fs.existsSync(location), 'directory completely removed') @@ -109,7 +105,7 @@ makeTest('test destroy() cleans and removes only leveldb parts of a dir', functi db.close(function (err) { t.ifError(err, 'no error from close()') - leveldown.destroy(location, function (err) { + ClassicLevel.destroy(location, function (err) { t.ifError(err, 'no error from destroy()') readfiletree(location, function (err, tree) { diff --git a/test/empty-range-option-test.js b/test/empty-range-option-test.js deleted file mode 100644 index d6a2fde..0000000 --- a/test/empty-range-option-test.js +++ /dev/null @@ -1,50 +0,0 @@ -const test = require('tape') -const concat = require('level-concat-iterator') -const testCommon = require('./common') -const rangeOptions = ['gt', 'gte', 'lt', 'lte'] - -test('empty range options are ignored', function (t) { - const db = testCommon.factory() - - t.test('setup', function (t) { - db.open(function (err) { - t.ifError(err, 'no open error') - - db.batch() - .put(Buffer.from([0]), 'value') - .put(Buffer.from([126]), 'value') - .write(t.end.bind(t)) - }) - }) - - rangeOptions.forEach(function (option) { - t.test(option, function (t) { - t.plan(8) - - concat(db.iterator({ [option]: Buffer.alloc(0) }), verifyBuffer) - concat(db.iterator({ [option]: Buffer.alloc(0), keyAsBuffer: false }), verifyString) - concat(db.iterator({ [option]: '' }), verifyBuffer) - concat(db.iterator({ [option]: '', keyAsBuffer: false }), verifyString) - - function verifyBuffer (err, entries) { - t.ifError(err, 'no concat error') - t.same(entries.map(getKey), [Buffer.from([0]), Buffer.from([126])]) - } - - function verifyString (err, entries) { - t.ifError(err, 'no concat error') - t.same(entries.map(getKey), ['\x00', '~']) - } - - function getKey (entry) { - return entry.key - } - }) - }) - - t.test('teardown', function (t) { - db.close(t.end.bind(t)) - }) - - t.end() -}) diff --git a/test/env-cleanup-hook.js b/test/env-cleanup-hook.js index 2c20a63..ee2d730 100644 --- a/test/env-cleanup-hook.js +++ b/test/env-cleanup-hook.js @@ -34,7 +34,7 @@ function test (steps) { if (err) throw err if (nextStep() === 'create-iterator') { - // Create an iterator, expected to be ended by the cleanup hook. + // Create an iterator, expected to be closed by the cleanup hook. const it = db.iterator() if (nextStep() === 'nexting') { diff --git a/test/getproperty-test.js b/test/getproperty-test.js index 6a8314b..9e7b8e5 100644 --- a/test/getproperty-test.js +++ b/test/getproperty-test.js @@ -5,8 +5,6 @@ const testCommon = require('./common') let db -test('setUp common', testCommon.setUp) - test('setUp db', function (t) { db = testCommon.factory() db.open(t.end.bind(t)) @@ -14,17 +12,17 @@ test('setUp db', function (t) { test('test argument-less getProperty() throws', function (t) { t.throws(db.getProperty.bind(db), { - name: 'Error', - message: 'getProperty() requires a valid `property` argument' - }, 'no-arg getProperty() throws') + name: 'TypeError', + message: "The first argument 'property' must be a string" + }) t.end() }) test('test non-string getProperty() throws', function (t) { t.throws(db.getProperty.bind(db, {}), { - name: 'Error', - message: 'getProperty() requires a valid `property` argument' - }, 'no-arg getProperty() throws') + name: 'TypeError', + message: "The first argument 'property' must be a string" + }) t.end() }) @@ -56,5 +54,12 @@ test('test invalid getProperty("leveldb.sstables")', function (t) { }) test('tearDown', function (t) { - db.close(testCommon.tearDown.bind(null, t)) + db.close(t.end.bind(t)) +}) + +test('getProperty() throws if db is closed', function (t) { + t.throws(() => db.getProperty('leveldb.stats'), { + code: 'LEVEL_DATABASE_NOT_OPEN' + }) + t.end() }) diff --git a/test/iterator-gc-test.js b/test/iterator-gc-test.js index 51a3dbd..0f52abb 100644 --- a/test/iterator-gc-test.js +++ b/test/iterator-gc-test.js @@ -1,7 +1,6 @@ 'use strict' const test = require('tape') -const collectEntries = require('level-concat-iterator') const testCommon = require('./common') const sourceData = [] @@ -13,8 +12,6 @@ for (let i = 0; i < 1e3; i++) { }) } -test('setUp', testCommon.setUp) - // When you have a database open with an active iterator, but no references to // the db, V8 will GC the database and you'll get an failed assert from LevelDB. test('db without ref does not get GCed while iterating', function (t) { @@ -50,7 +47,7 @@ test('db without ref does not get GCed while iterating', function (t) { function iterate (it) { // No reference to db here, could be GCed. It shouldn't.. - collectEntries(it, function (err, entries) { + it.all(function (err, entries) { t.ifError(err, 'no iterator error') t.is(entries.length, sourceData.length, 'got data') @@ -64,5 +61,3 @@ test('db without ref does not get GCed while iterating', function (t) { }) } }) - -test('tearDown', testCommon.tearDown) diff --git a/test/iterator-recursion-test.js b/test/iterator-recursion-test.js index 3135c5d..fa84396 100644 --- a/test/iterator-recursion-test.js +++ b/test/iterator-recursion-test.js @@ -22,8 +22,6 @@ const sourceData = (function () { return d }()) -test('setUp common', testCommon.setUp) - // TODO: fix this test. It asserted that we didn't segfault if user code had an // infinite loop leading to stack exhaustion, which caused a node::FatalException() // call in our Iterator to segfault. This was fixed in 2014 (commit 85e6a38). @@ -84,5 +82,5 @@ test('iterate over a large iterator with a large watermark', function (t) { }) test('tearDown', function (t) { - db.close(testCommon.tearDown.bind(null, t)) + db.close(t.end.bind(t)) }) diff --git a/test/iterator-starvation-test.js b/test/iterator-starvation-test.js index 08b8571..f870e6b 100644 --- a/test/iterator-starvation-test.js +++ b/test/iterator-starvation-test.js @@ -14,8 +14,6 @@ for (let i = 0; i < 1e4; i++) { }) } -test('setUp', testCommon.setUp) - test('iterator does not starve event loop', function (t) { t.plan(6) @@ -90,7 +88,7 @@ test('iterator with seeks does not starve event loop', function (t) { if (err || (key === undefined && value === undefined)) { t.ifError(err, 'no next error') t.is(entries, sourceData.length, 'got all data') - t.is(breaths, sourceData.length, 'breathed while iterating') + t.is(breaths, sourceData.length - 1, 'breathed while iterating') return db.close(function (err) { t.ifError(err, 'no close error') @@ -120,5 +118,3 @@ test('iterator with seeks does not starve event loop', function (t) { }) }) }) - -test('tearDown', testCommon.tearDown) diff --git a/test/iterator-test.js b/test/iterator-test.js index 413430a..50706d0 100644 --- a/test/iterator-test.js +++ b/test/iterator-test.js @@ -1,30 +1,6 @@ -const make = require('./make') - -// This test isn't included in abstract-leveldown because -// the empty-check is currently performed by leveldown. -make('iterator#seek throws if target is empty', function (db, t, done) { - const targets = ['', Buffer.alloc(0), []] - let pending = targets.length - - targets.forEach(function (target) { - const ite = db.iterator() - let error - - try { - ite.seek(target) - } catch (err) { - error = err.message - } - - t.is(error, 'cannot seek() to an empty target', 'got error') - ite.end(end) - }) +'use strict' - function end (err) { - t.ifError(err, 'no error from end()') - if (!--pending) done() - } -}) +const make = require('./make') make('iterator optimized for seek', function (db, t, done) { const batch = db.batch() @@ -41,72 +17,25 @@ make('iterator optimized for seek', function (db, t, done) { ite.next(function (err, key, value) { t.ifError(err, 'no error from next()') t.equal(key.toString(), 'a', 'key matches') - t.equal(ite.cache.length, 0, 'no cache') + t.equal(ite.cached, 0, 'no cache') ite.next(function (err, key, value) { t.ifError(err, 'no error from next()') t.equal(key.toString(), 'b', 'key matches') - t.ok(ite.cache.length > 0, 'has cached items') + t.ok(ite.cached > 0, 'has cached items') ite.seek('d') - t.notOk(ite.cache, 'cache is removed') + t.is(ite.cached, 0, 'cache is emptied') ite.next(function (err, key, value) { t.ifError(err, 'no error from next()') t.equal(key.toString(), 'd', 'key matches') - t.equal(ite.cache.length, 0, 'no cache') + t.equal(ite.cached, 0, 'no cache') ite.next(function (err, key, value) { t.ifError(err, 'no error from next()') t.equal(key.toString(), 'e', 'key matches') - t.ok(ite.cache.length > 0, 'has cached items') - ite.end(done) + t.ok(ite.cached > 0, 'has cached items') + ite.close(done) }) }) }) }) }) }) - -make('close db with open iterator', function (db, t, done) { - const ite = db.iterator() - let cnt = 0 - let hadError = false - - ite.next(function loop (err, key, value) { - if (cnt++ === 0) { - // The first call should succeed, because it was scheduled before close() - t.ifError(err, 'no error from next()') - } else { - // The second call should fail, because it was scheduled after close() - t.equal(err.message, 'iterator has ended') - hadError = true - } - if (key !== undefined) { ite.next(loop) } - }) - - db.close(function (err) { - t.ifError(err, 'no error from close()') - t.ok(hadError) - - done(null, false) - }) -}) - -make('key-only iterator', function (db, t, done) { - const it = db.iterator({ values: false, keyAsBuffer: false, valueAsBuffer: false }) - - it.next(function (err, key, value) { - t.ifError(err, 'no next() error') - t.is(key, 'one') - t.is(value, '') // should this be undefined? - it.end(done) - }) -}) - -make('value-only iterator', function (db, t, done) { - const it = db.iterator({ keys: false, keyAsBuffer: false, valueAsBuffer: false }) - - it.next(function (err, key, value) { - t.ifError(err, 'no next() error') - t.is(key, '') // should this be undefined? - t.is(value, '1') - it.end(done) - }) -}) diff --git a/test/leak-tester-iterator.js b/test/leak-tester-iterator.js index 2cf1945..50ca019 100644 --- a/test/leak-tester-iterator.js +++ b/test/leak-tester-iterator.js @@ -15,7 +15,7 @@ function run () { it.next(function (err) { if (err) throw err - it.end(function (err) { + it.close(function (err) { if (err) throw err if (!rssBase) { diff --git a/test/leveldown-test.js b/test/leveldown-test.js deleted file mode 100644 index d1de19c..0000000 --- a/test/leveldown-test.js +++ /dev/null @@ -1,13 +0,0 @@ -'use strict' - -const test = require('tape') -const leveldown = require('..') - -test('test database creation non-string location throws', function (t) { - t.throws( - leveldown.bind(null, {}), - /constructor requires a location string argument/, - 'non-string location leveldown() throws' - ) - t.end() -}) diff --git a/test/location-test.js b/test/location-test.js new file mode 100644 index 0000000..2b5bc0f --- /dev/null +++ b/test/location-test.js @@ -0,0 +1,16 @@ +'use strict' + +const test = require('tape') +const { ClassicLevel } = require('..') + +test('test database creation non-string location throws', function (t) { + t.throws(() => new ClassicLevel({}), { + name: 'TypeError', + message: "The first argument 'location' must be a non-empty string" + }) + t.throws(() => new ClassicLevel(''), { + name: 'TypeError', + message: "The first argument 'location' must be a non-empty string" + }) + t.end() +}) diff --git a/test/repair-test.js b/test/repair-test.js index f7ff60a..68b892c 100644 --- a/test/repair-test.js +++ b/test/repair-test.js @@ -2,27 +2,23 @@ const test = require('tape') const fs = require('fs') -const leveldown = require('..') +const { ClassicLevel } = require('..') const makeTest = require('./make') -test('test argument-less repair() throws', function (t) { - t.throws(leveldown.repair, { - name: 'Error', - message: 'repair() requires `location` and `callback` arguments' - }, 'no-arg repair() throws') - t.end() -}) - -test('test callback-less, 1-arg, repair() throws', function (t) { - t.throws(leveldown.repair.bind(null, 'foo'), { - name: 'Error', - message: 'repair() requires `location` and `callback` arguments' - }, 'callback-less, 1-arg repair() throws') +test('test repair() without location throws', function (t) { + t.throws(ClassicLevel.repair, { + name: 'TypeError', + message: "The first argument 'location' must be a non-empty string" + }) + t.throws(() => ClassicLevel.repair(''), { + name: 'TypeError', + message: "The first argument 'location' must be a non-empty string" + }) t.end() }) test('test repair non-existent directory returns error', function (t) { - leveldown.repair('/1/2/3/4', function (err) { + ClassicLevel.repair('/1/2/3/4', function (err) { if (process.platform !== 'win32') { t.ok(/no such file or directory/i.test(err), 'error on callback') } else { @@ -43,7 +39,7 @@ makeTest('test repair() compacts', function (db, t, done) { t.ok(files.some(function (f) { return (/\.log$/).test(f) }), 'directory contains log file(s)') t.notOk(files.some(function (f) { return (/\.ldb$/).test(f) }), 'directory does not contain ldb file(s)') - leveldown.repair(location, function (err) { + ClassicLevel.repair(location, function (err) { t.ifError(err, 'no error from repair()') files = fs.readdirSync(location) diff --git a/test/segfault-test.js b/test/segfault-test.js index d0ce119..dce70a4 100644 --- a/test/segfault-test.js +++ b/test/segfault-test.js @@ -87,20 +87,3 @@ testPending('operations', operations.length, function (db, next) { fn(db, next) } }) - -// See https://github.com/Level/leveldown/issues/134 -test('iterator() does not segfault if db is not open', function (t) { - t.plan(2) - - const db = testCommon.factory() - - try { - db.iterator() - } catch (err) { - t.is(err.message, 'cannot call iterator() before open()') - } - - db.close(function (err) { - t.ifError(err, 'no close error') - }) -})