diff --git a/index.js b/index.js index 4792856..29c51d9 100644 --- a/index.js +++ b/index.js @@ -10,13 +10,27 @@ var toBuffer = require('typedarray-to-buffer') function Level(location) { if (!(this instanceof Level)) return new Level(location) - if (!location) throw new Error("constructor requires at least a location argument") + AbstractLevelDOWN.call(this, location) this.IDBOptions = {} this.location = location } util.inherits(Level, AbstractLevelDOWN) +// Detect binary key support (IndexedDB Second Edition) +Level.binaryKeys = (function () { + if (typeof indexedDB === 'undefined') { + return false + } + + try { + indexedDB.cmp(new Uint8Array(0), 0) + return true + } catch (err) { + return false + } +})() + Level.prototype._open = function(options, callback) { var self = this @@ -43,14 +57,12 @@ Level.prototype._get = function (key, options, callback) { // 'NotFound' error, consistent with LevelDOWN API return callback(new Error('NotFound')) } - // by default return buffers, unless explicitly told not to - var asBuffer = true - if (options.asBuffer === false) asBuffer = false - if (options.raw) asBuffer = false - if (asBuffer) { + + if (options.asBuffer) { if (value instanceof Uint8Array) value = toBuffer(value) else value = Buffer.from(String(value)) } + return callback(null, value, key) }, callback) } @@ -60,28 +72,42 @@ Level.prototype._del = function(id, options, callback) { } Level.prototype._put = function (key, value, options, callback) { - // TODO: once we upgrade abstract-leveldown, it will call _serializeValue for us. - value = this._serializeValue(value, options) this.idb.put(key, value, function() { callback() }, callback) } -// NOTE: doesn't match abstract signature yet (which has no options argument). -Level.prototype._serializeValue = function (value, options) { +// Valid key types in IndexedDB Second Edition: +// +// - Number, except NaN. Includes Infinity and -Infinity +// - Date, except invalid (NaN) +// - String +// - ArrayBuffer or a view thereof (typed arrays) +// - Array, except cyclical and empty (e.g. Array(10)). Elements must be valid +// types themselves. +Level.prototype._serializeKey = function (key) { // TODO: do we still need to support ArrayBuffer? - if (value instanceof ArrayBuffer) return Buffer.from(value) - if (value == null) return '' + if (key instanceof ArrayBuffer) { + key = Buffer.from(key) + return Level.binaryKeys ? key : key.toString() + } else if (Buffer.isBuffer(key)) { + return Level.binaryKeys ? key : key.toString() + } else if (Array.isArray(key)) { + return key.map(this._serializeKey, this) + } else if ((typeof key === 'number' || key instanceof Date) && !isNaN(key)) { + return key + } - // TODO: remove - if (options.raw) return value + return String(key) +} - // TODO: remove - if (typeof value !== 'object') return value.toString() +Level.prototype._serializeValue = function (value) { + // TODO: do we still need to support ArrayBuffer? + if (value instanceof ArrayBuffer) return Buffer.from(value) + if (value == null) return '' return value } -Level.prototype.iterator = function (options) { - if (typeof options !== 'object') options = {} +Level.prototype._iterator = function (options) { return new Iterator(this.idb, options) } @@ -100,9 +126,6 @@ Level.prototype._batch = function (array, options, callback) { currentOp = array[i] modified[i] = copiedOp - // TODO: once we upgrade abstract-leveldown, it will call _serializeValue for us. - currentOp.value = this._serializeValue(currentOp.value, options) - for (k in currentOp) { if (k === 'type' && currentOp[k] == 'del') { copiedOp[k] = 'remove' @@ -120,18 +143,6 @@ Level.prototype._close = function (callback) { callback() } -Level.prototype._approximateSize = function (start, end, callback) { - var err = new Error('Not implemented') - if (callback) - return callback(err) - - throw err -} - -Level.prototype._isBuffer = function (obj) { - return Buffer.isBuffer(obj) -} - Level.destroy = function (db, callback) { if (typeof db === 'object') { var prefix = db.IDBOptions.storePrefix || 'IDBWrapper-' diff --git a/iterator.js b/iterator.js index 579755f..e164fb2 100644 --- a/iterator.js +++ b/iterator.js @@ -18,15 +18,20 @@ function Iterator (db, options) { AbstractIterator.call(this, db) this._order = options.reverse ? 'DESC': 'ASC' - this._limit = options.limit || -1 + this._limit = options.limit this._count = 0 this._callback = null this._cache = [] this._completed = false + this.transaction = null - // TODO: in later abstract-leveldown, these have proper defaults - this._keyAsBuffer = options.keyAsBuffer !== false - this._valueAsBuffer = options.valueAsBuffer !== false + this._keyAsBuffer = options.keyAsBuffer + this._valueAsBuffer = options.valueAsBuffer + + if (this._limit === 0) { + this._completed = true + return + } var lower = ltgt.lowerBound(options) var upper = ltgt.upperBound(options) @@ -98,7 +103,7 @@ Iterator.prototype._next = function (callback) { // TODO: can remove this after upgrading abstract-leveldown if (!callback) throw new Error('next() requires a callback argument') - if (this.transaction.error !== null) { + if (this.transaction !== null && this.transaction.error !== null) { var err = this.transaction.error setTimeout(function() { diff --git a/package.json b/package.json index fb22981..bb4b2ec 100644 --- a/package.json +++ b/package.json @@ -21,11 +21,12 @@ "devDependencies": { "airtap": "0.0.5", "browserify": "~16.2.2", - "levelup": "~0.18.2", + "encoding-down": "~5.0.2", + "levelup": "~3.0.0", "tape": "^4.0.0" }, "dependencies": { - "abstract-leveldown": "~0.12.0", + "abstract-leveldown": "~5.0.0", "idb-wrapper": "^1.5.0", "isbuffer": "~0.0.0", "ltgt": "^2.1.2", diff --git a/test/custom-tests.js b/test/custom-tests.js index e347979..234b42c 100644 --- a/test/custom-tests.js +++ b/test/custom-tests.js @@ -3,9 +3,7 @@ var levelup = require('levelup') module.exports.all = function(leveljs, tape, testCommon) { tape('setUp', testCommon.setUp) - // This is covered by abstract-leveldown tests, but we're - // not on latest yet, so this is insurance. - tape('store buffer value', function(t) { + tape('buffer value', function(t) { var level = leveljs(testCommon.location()) level.open(function(err) { t.notOk(err, 'no error') @@ -21,15 +19,79 @@ module.exports.all = function(leveljs, tape, testCommon) { }) }) - // TODO: this should be supported without raw: true. Which is possible once - // we upgrade abstract-leveldown (which only tests strings and Buffers now). - tape('store native JS types with raw = true', function(t) { + // This should be covered by abstract-leveldown tests, but that's + // prevented by process.browser checks (Level/abstract-leveldown#121). + // This test is adapted from memdown. + leveljs.binaryKeys && tape('buffer keys', function (t) { + var db = leveljs(testCommon.location()) + + db.open(function (err) { + t.ifError(err, 'no open error') + + var one = Buffer.from('80', 'hex') + var two = Buffer.from('c0', 'hex') + + t.ok(two.toString() === one.toString(), 'would be equal when not buffer-aware') + t.ok(Buffer.compare(two, one) > 0, 'but greater when buffer-aware') + + db.put(one, 'one', function (err) { + t.notOk(err, 'no error') + + db.get(one, { asBuffer: false }, function (err, value) { + t.notOk(err, 'no error') + t.equal(value, 'one', 'value one ok') + + db.put(two, 'two', function (err) { + t.notOk(err, 'no error') + + db.get(one, { asBuffer: false }, function (err, value) { + t.notOk(err, 'no error') + t.equal(value, 'one', 'value one is the same') + + db.close(function (err) { + t.ifError(err, 'no close error') + t.end() + }) + }) + }) + }) + }) + }) + }) + + // Adapted from a memdown test. + leveljs.binaryKeys && tape('iterator stringifies buffer input', function (t) { + t.plan(6) + + var db = leveljs(testCommon.location()) + + db.open(function (err) { + t.ifError(err, 'no open error') + + db.put(1, 2, function (err) { + t.ifError(err, 'no put error') + + testCommon.collectEntries(db.iterator(), function (err, entries) { + t.ifError(err, 'no iterator error') + t.same(entries[0].key, Buffer.from('1'), 'key is stringified') + t.same(entries[0].value, Buffer.from('2'), 'value is stringified') + + db.close(function (err) { + t.ifError(err, 'no close error') + }) + }) + }) + }) + }) + + // TODO: merge this and the test below. Test all types. + tape('store native JS types', function(t) { var level = leveljs(testCommon.location()) level.open(function(err) { t.notOk(err, 'no error') - level.put('key', true, { raw: true }, function (err) { + level.put('key', true, function (err) { t.notOk(err, 'no error') - level.get('key', { raw: true }, function(err, value) { + level.get('key', { asBuffer: false }, function(err, value) { t.notOk(err, 'no error') t.ok(typeof value === 'boolean', 'is boolean type') t.ok(value, 'is truthy') @@ -45,9 +107,9 @@ module.exports.all = function(leveljs, tape, testCommon) { var level = leveljs(testCommon.location()) level.open(function(err) { t.notOk(err, 'no error') - level.put('key', NaN, { raw: true }, function (err) { + level.put('key', NaN, function (err) { t.notOk(err, 'no error') - level.get('key', { raw: true }, function(err, value) { + level.get('key', { asBuffer: false }, function(err, value) { t.notOk(err, 'no error') t.ok(Number.isNaN(value), 'is NaN') level.close(t.end.bind(t)) @@ -61,20 +123,20 @@ module.exports.all = function(leveljs, tape, testCommon) { tape('test levelup .destroy w/ string', function(t) { var location = testCommon.location() - var level = levelup(location, {db: leveljs}) - level.put('key', 'value', function (err) { + var db = levelup(leveljs(location)) + db.put('key', 'value', function (err) { t.notOk(err, 'no error') - level.get('key', function (err, value) { + db.get('key', { asBuffer: false }, function (err, value) { t.notOk(err, 'no error') t.equal(value, 'value', 'should have value') - level.close(function (err) { + db.close(function (err) { t.notOk(err, 'no error') leveljs.destroy(location, function (err) { t.notOk(err, 'no error') - var level2 = levelup(location, {db: leveljs}) - level2.get('key', function (err, value) { - t.ok(err, 'key is not there') - level2.close(t.end.bind(t)) + var db2 = levelup(leveljs(location)) + db2.get('key', { asBuffer: false }, function (err, value) { + t.ok(err && err.notFound, 'key is not there') + db2.close(t.end.bind(t)) }) }) }) @@ -84,20 +146,20 @@ module.exports.all = function(leveljs, tape, testCommon) { tape('test levelup .destroy w/ db instance', function(t) { var location = testCommon.location() - var level = levelup(location, {db: leveljs}) - level.put('key', 'value', function (err) { + var db = levelup(leveljs(location)) + db.put('key', 'value', function (err) { t.notOk(err, 'no error') - level.get('key', function (err, value) { + db.get('key', { asBuffer: false }, function (err, value) { t.notOk(err, 'no error') t.equal(value, 'value', 'should have value') - level.close(function (err) { + db.close(function (err) { t.notOk(err, 'no error') - leveljs.destroy(level.db, function (err) { + leveljs.destroy(db.db, function (err) { t.notOk(err, 'no error') - var level2 = levelup(location, {db: leveljs}) - level2.get('key', function (err, value) { - t.ok(err, 'key is not there') - level2.close(t.end.bind(t)) + var db2 = levelup(leveljs(location)) + db2.get('key', { asBuffer: false }, function (err, value) { + t.ok(err && err.notFound, 'key is not there') + db2.close(t.end.bind(t)) }) }) }) diff --git a/test/test-levelup.js b/test/test-levelup.js index 9e117e7..6b08230 100644 --- a/test/test-levelup.js +++ b/test/test-levelup.js @@ -1,27 +1,25 @@ /*** Levelup tests - Temporary to test integration, can be removed later. + NOTE: Temporary integration tests, can be removed later. Remember + to also remove the encoding-down devDependency. ***/ var levelup = require('levelup') +var encoding = require('encoding-down') var leveljs = require('../') var test = require('tape') var testCommon = require('./testCommon') -function identity (v) { - return v -} - test('setup', testCommon.setUp) test('levelup put', function (t) { t.plan(4) - // TODO: update signature after upgrading levelup - var db = levelup(testCommon.location(), { db: leveljs }) + var down = leveljs(testCommon.location()) + var db = levelup(down) db.put('name', 'LevelUP string', function (err) { t.ifError(err, 'no put error') - db.get('name', function (err, value) { + db.get('name', { asBuffer: false }, function (err, value) { t.ifError(err, 'no get error') t.is(value, 'LevelUP string') @@ -33,10 +31,10 @@ test('levelup put', function (t) { }) test('binary', function (t) { - t.plan(6) + t.plan(9) - // TODO: update signature after upgrading levelup - var db = levelup(testCommon.location(), { db: leveljs, valueEncoding: 'binary' }) + var down = leveljs(testCommon.location()) + var db = levelup(encoding(down, { valueEncoding: 'binary' })) var buf = Buffer.from('00ff', 'hex') db.put('binary', buf, function (err) { @@ -44,14 +42,14 @@ test('binary', function (t) { db.get('binary', function (err, value) { t.ifError(err, 'no get error') + t.ok(Buffer.isBuffer(value), 'is a buffer') + t.same(value, buf) - // This levelup is really old and its binary decoder does: - // `return process.browser ? buffer.toString(type) : buffer` - t.notOk(Buffer.isBuffer(value), 'not a buffer') - - db.get('binary', { valueEncoding: { decode: identity } }, function (err, value) { + db.get('binary', { valueEncoding: 'id' }, function (err, value) { t.ifError(err, 'no get error') - t.ok(Buffer.isBuffer(value), 'is a buffer') + t.notOk(Buffer.isBuffer(value), 'is not a buffer') + t.ok(value instanceof Uint8Array, 'is a Uint8Array') + t.same(Buffer.from(value), buf) db.close(function (err) { t.ifError(err, 'no close error') diff --git a/test/test.js b/test/test.js index 28ab3dc..77893e2 100644 --- a/test/test.js +++ b/test/test.js @@ -5,24 +5,18 @@ var testCommon = require('./testCommon') // load IndexedDBShim in the tests require('./idb-shim.js')() -var testBuffer = Buffer.from('foo') - /*** compatibility with basic LevelDOWN API ***/ require('abstract-leveldown/abstract/leveldown-test').args(leveljs, tape, testCommon) require('abstract-leveldown/abstract/open-test').open(leveljs, tape, testCommon) require('abstract-leveldown/abstract/put-test').all(leveljs, tape, testCommon) require('abstract-leveldown/abstract/del-test').all(leveljs, tape, testCommon) require('abstract-leveldown/abstract/get-test').all(leveljs, tape, testCommon) -require('abstract-leveldown/abstract/put-get-del-test').all(leveljs, tape, testCommon, testBuffer) +require('abstract-leveldown/abstract/put-get-del-test').all(leveljs, tape, testCommon) require('abstract-leveldown/abstract/batch-test').all(leveljs, tape, testCommon) require('abstract-leveldown/abstract/chained-batch-test').all(leveljs, tape, testCommon) require('abstract-leveldown/abstract/close-test').close(leveljs, tape, testCommon) require('abstract-leveldown/abstract/iterator-test').all(leveljs, tape, testCommon) - -// NOTE: exclude this because the handling of buffers is inconsistent between -// iterator-test and ranges-test. We can't make both pass, but that's OK, as -// ranges-test is removed in a later abstract-leveldown version anyway. -// require('abstract-leveldown/abstract/ranges-test').all(leveljs, tape, testCommon) +require('abstract-leveldown/abstract/iterator-range-test').all(leveljs, tape, testCommon) // non abstract-leveldown tests: require('./custom-tests.js').all(leveljs, tape, testCommon)