diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..43c97e7 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/README.md b/README.md index 59c83a4..4b73abe 100644 --- a/README.md +++ b/README.md @@ -12,21 +12,6 @@ [![Common Changelog](https://common-changelog.org/badge.svg)](https://common-changelog.org) [![Donate](https://img.shields.io/badge/donate-orange?logo=open-collective\&logoColor=fff)](https://opencollective.com/level) -## Table of Contents - -
Click to expand - -- [Usage](#usage) -- [API](#api) - - [`db = new BrowserLevel(location[, options])`](#db--new-browserlevellocation-options) - - [`BrowserLevel.destroy(location[, prefix][, callback])`](#browserleveldestroylocation-prefix-callback) -- [Install](#install) -- [Contributing](#contributing) -- [Donate](#donate) -- [License](#license) - -
- ## Usage ```js @@ -92,9 +77,9 @@ Besides `abstract-level` options, the optional `options` argument may contain: See [`IDBFactory#open()`](https://developer.mozilla.org/en-US/docs/Web/API/IDBFactory/open) for more details about database name and version. -### `BrowserLevel.destroy(location[, prefix][, callback])` +### `BrowserLevel.destroy(location[, prefix])` -Delete the IndexedDB database at the given `location`. If `prefix` is not given, it defaults to the same value as the `BrowserLevel` constructor does. The `callback` function will be called when the destroy operation is complete, with a possible error argument. If no callback is provided, a promise is returned. This method is an additional method that is not part of the [`abstract-level`](https://github.com/Level/abstract-level) interface. +Delete the IndexedDB database at the given `location`. Returns a promise. If `prefix` is not given, it defaults to the same value as the `BrowserLevel` constructor does. This method is an additional method that is not part of the [`abstract-level`](https://github.com/Level/abstract-level) interface. Before calling `destroy()`, close a database if it's using the same `location` and `prefix`: diff --git a/UPGRADING.md b/UPGRADING.md index 5762168..9df81ca 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -2,6 +2,10 @@ This document describes breaking changes and how to upgrade. For a complete list of changes including minor and patch releases, please refer to the [changelog](CHANGELOG.md). +## 2.0.0 + +This release upgrades to `abstract-level` 2 which adds [hooks](https://github.com/Level/abstract-level#hooks) and drops callbacks and not-found errors. Please refer to the [upgrade guide of `abstract-level`](https://github.com/Level/abstract-level/blob/v2.0.0/UPGRADING.md) for details. + ## 1.0.0 **Introducing `browser-level`: a fork of [`level-js`](https://github.com/Level/level-js) that removes the need for [`levelup`](https://github.com/Level/levelup) and more. It implements the [`abstract-level`](https://github.com/Level/abstract-level) interface instead of [`abstract-leveldown`](https://github.com/Level/abstract-leveldown) and thus has the same API as `level` and `levelup` including encodings, promises and events. In addition, you can now choose to use Uint8Array instead of Buffer. Sublevels are builtin.** diff --git a/index.d.ts b/index.d.ts index 1cdbc92..6404c59 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,7 +1,6 @@ import { AbstractLevel, AbstractDatabaseOptions, - NodeCallback, AbstractOpenOptions, AbstractGetOptions, AbstractGetManyOptions, @@ -58,8 +57,6 @@ export class BrowserLevel */ static destroy (location: string): Promise static destroy (location: string, prefix: string): Promise - static destroy (location: string, callback: NodeCallback): void - static destroy (location: string, prefix: string, callback: NodeCallback): void } /** diff --git a/index.js b/index.js index 43c95a1..41e19f1 100644 --- a/index.js +++ b/index.js @@ -4,8 +4,6 @@ const { AbstractLevel } = require('abstract-level') const ModuleError = require('module-error') -const parallel = require('run-parallel-limit') -const { fromCallback } = require('catering') const { Iterator } = require('./iterator') const deserialize = require('./util/deserialize') const clear = require('./util/clear') @@ -20,7 +18,6 @@ const kLocation = Symbol('location') const kVersion = Symbol('version') const kStore = Symbol('store') const kOnComplete = Symbol('onComplete') -const kPromise = Symbol('promise') class BrowserLevel extends AbstractLevel { constructor (location, options, _) { @@ -73,25 +70,27 @@ class BrowserLevel extends AbstractLevel { return 'browser-level' } - _open (options, callback) { - const req = indexedDB.open(this[kNamePrefix] + this[kLocation], this[kVersion]) + async _open (options) { + const request = indexedDB.open(this[kNamePrefix] + this[kLocation], this[kVersion]) - req.onerror = function () { - callback(req.error || new Error('unknown error')) - } - - req.onsuccess = () => { - this[kIDB] = req.result - callback() - } - - req.onupgradeneeded = (ev) => { + request.onupgradeneeded = (ev) => { const db = ev.target.result if (!db.objectStoreNames.contains(this[kLocation])) { db.createObjectStore(this[kLocation]) } } + + return new Promise((resolve, reject) => { + request.onerror = function () { + reject(request.error || new Error('unknown error')) + } + + request.onsuccess = () => { + this[kIDB] = request.result + resolve() + } + }) } [kStore] (mode) { @@ -99,94 +98,91 @@ class BrowserLevel extends AbstractLevel { return transaction.objectStore(this[kLocation]) } - [kOnComplete] (request, callback) { + [kOnComplete] (request) { const transaction = request.transaction - // Take advantage of the fact that a non-canceled request error aborts - // the transaction. I.e. no need to listen for "request.onerror". - transaction.onabort = function () { - callback(transaction.error || new Error('aborted by user')) - } + return new Promise(function (resolve, reject) { + // Take advantage of the fact that a non-canceled request error aborts + // the transaction. I.e. no need to listen for "request.onerror". + transaction.onabort = function () { + reject(transaction.error || new Error('aborted by user')) + } - transaction.oncomplete = function () { - callback(null, request.result) - } + transaction.oncomplete = function () { + resolve(request.result) + } + }) } - _get (key, options, callback) { + async _get (key, options) { const store = this[kStore]('readonly') - let req - - try { - req = store.get(key) - } catch (err) { - return this.nextTick(callback, err) - } + const request = store.get(key) + const value = await this[kOnComplete](request) - this[kOnComplete](req, function (err, value) { - if (err) return callback(err) - - if (value === undefined) { - return callback(new ModuleError('Entry not found', { - code: 'LEVEL_NOT_FOUND' - })) - } - - callback(null, deserialize(value)) - }) + return deserialize(value) } - _getMany (keys, options, callback) { + async _getMany (keys, options) { const store = this[kStore]('readonly') - const tasks = keys.map((key) => (next) => { - let request + const iterator = keys.values() + + // Consume the iterator with N parallel worker bees + const n = Math.min(16, keys.length) + const bees = new Array(n) + const values = new Array(keys.length) + + let keyIndex = 0 + let abort = false + const bee = async function () { try { - request = store.get(key) + for (const key of iterator) { + if (abort) break + + const valueIndex = keyIndex++ + const request = store.get(key) + + await new Promise(function (resolve, reject) { + request.onsuccess = () => { + values[valueIndex] = deserialize(request.result) + resolve() + } + + request.onerror = (ev) => { + ev.stopPropagation() + reject(request.error) + } + }) + } } catch (err) { - return next(err) - } - - request.onsuccess = () => { - const value = request.result - next(null, value === undefined ? value : deserialize(value)) + abort = true + throw err } + } - request.onerror = (ev) => { - ev.stopPropagation() - next(request.error) - } - }) + for (let i = 0; i < n; i++) { + bees[i] = bee() + } - parallel(tasks, 16, callback) + await Promise.allSettled(bees) + return values } - _del (key, options, callback) { + async _del (key, options) { const store = this[kStore]('readwrite') - let req - - try { - req = store.delete(key) - } catch (err) { - return this.nextTick(callback, err) - } + const request = store.delete(key) - this[kOnComplete](req, callback) + return this[kOnComplete](request) } - _put (key, value, options, callback) { + async _put (key, value, options) { const store = this[kStore]('readwrite') - let req - try { - // Will throw a DataError or DataCloneError if the environment - // does not support serializing the key or value respectively. - req = store.put(value, key) - } catch (err) { - return this.nextTick(callback, err) - } + // Will throw a DataError or DataCloneError if the environment + // does not support serializing the key or value respectively. + const request = store.put(value, key) - this[kOnComplete](req, callback) + return this[kOnComplete](request) } // TODO: implement key and value iterators @@ -194,19 +190,19 @@ class BrowserLevel extends AbstractLevel { return new Iterator(this, this[kLocation], options) } - _batch (operations, options, callback) { + async _batch (operations, options) { const store = this[kStore]('readwrite') const transaction = store.transaction let index = 0 let error - transaction.onabort = function () { - callback(error || transaction.error || new Error('aborted by user')) - } + const promise = new Promise(function (resolve, reject) { + transaction.onabort = function () { + reject(error || transaction.error || new Error('aborted by user')) + } - transaction.oncomplete = function () { - callback() - } + transaction.oncomplete = resolve + }) // Wait for a request to complete before making the next, saving CPU. function loop () { @@ -232,60 +228,48 @@ class BrowserLevel extends AbstractLevel { } loop() + return promise } - _clear (options, callback) { + async _clear (options) { let keyRange - let req try { keyRange = createKeyRange(options) } catch (e) { // The lower key is greater than the upper key. // IndexedDB throws an error, but we'll just do nothing. - return this.nextTick(callback) + return } if (options.limit >= 0) { // IDBObjectStore#delete(range) doesn't have such an option. // Fall back to cursor-based implementation. - return clear(this, this[kLocation], keyRange, options, callback) + return clear(this, this[kLocation], keyRange, options) } - try { - const store = this[kStore]('readwrite') - req = keyRange ? store.delete(keyRange) : store.clear() - } catch (err) { - return this.nextTick(callback, err) - } + const store = this[kStore]('readwrite') + const request = keyRange ? store.delete(keyRange) : store.clear() - this[kOnComplete](req, callback) + return this[kOnComplete](request) } - _close (callback) { + async _close () { this[kIDB].close() - this.nextTick(callback) } } -BrowserLevel.destroy = function (location, prefix, callback) { - if (typeof prefix === 'function') { - callback = prefix +BrowserLevel.destroy = async function (location, prefix) { + if (prefix == null) { prefix = DEFAULT_PREFIX } - callback = fromCallback(callback, kPromise) const request = indexedDB.deleteDatabase(prefix + location) - request.onsuccess = function () { - callback() - } - - request.onerror = function (err) { - callback(err) - } - - return callback[kPromise] + return new Promise(function (resolve, reject) { + request.onsuccess = resolve + request.onerror = reject + }) } exports.BrowserLevel = BrowserLevel diff --git a/iterator.js b/iterator.js index 6b84425..245163a 100644 --- a/iterator.js +++ b/iterator.js @@ -28,15 +28,17 @@ class Iterator extends AbstractIterator { // Note: if called by _all() then size can be Infinity. This is an internal // detail; by design AbstractIterator.nextv() does not support Infinity. - _nextv (size, options, callback) { + async _nextv (size, options) { this[kFirst] = false if (this[kFinished]) { - return this.nextTick(callback, null, []) - } else if (this[kCache].length > 0) { + return [] + } + + if (this[kCache].length > 0) { // TODO: mixing next and nextv is not covered by test suite size = Math.min(size, this[kCache].length) - return this.nextTick(callback, null, this[kCache].splice(0, size)) + return this[kCache].splice(0, size) } // Adjust range by what we already visited @@ -58,13 +60,24 @@ class Iterator extends AbstractIterator { // The lower key is greater than the upper key. // IndexedDB throws an error, but we'll just return 0 results. this[kFinished] = true - return this.nextTick(callback, null, []) + return [] } const transaction = this.db.db.transaction([this[kLocation]], 'readonly') const store = transaction.objectStore(this[kLocation]) const entries = [] + const promise = new Promise(function (resolve, reject) { + // If an error occurs (on the request), the transaction will abort. + transaction.onabort = () => { + reject(transaction.error || new Error('aborted by user')) + } + + transaction.oncomplete = () => { + resolve(entries) + } + }) + if (!this[kOptions].reverse) { let keys let values @@ -90,8 +103,8 @@ class Iterator extends AbstractIterator { const value = values[i] entries[i] = [ - this[kOptions].keys && key !== undefined ? deserialize(key) : undefined, - this[kOptions].values && value !== undefined ? deserialize(value) : undefined + this[kOptions].keys ? deserialize(key) : undefined, + this[kOptions].values ? deserialize(value) : undefined ] } @@ -107,7 +120,7 @@ class Iterator extends AbstractIterator { } } else { keys = [] - this.nextTick(complete) + complete() } if (this[kOptions].values) { @@ -117,7 +130,7 @@ class Iterator extends AbstractIterator { } } else { values = [] - this.nextTick(complete) + complete() } } else { // Can't use getAll() in reverse, so use a slower cursor that yields one item at a time @@ -147,25 +160,15 @@ class Iterator extends AbstractIterator { } } - // If an error occurs (on the request), the transaction will abort. - transaction.onabort = () => { - callback(transaction.error || new Error('aborted by user')) - callback = null - } - - transaction.oncomplete = () => { - callback(null, entries) - callback = null - } + return promise } - _next (callback) { + async _next () { if (this[kCache].length > 0) { - const [key, value] = this[kCache].shift() - this.nextTick(callback, null, key, value) - } else if (this[kFinished]) { - this.nextTick(callback) - } else { + return this[kCache].shift() + } + + if (!this[kFinished]) { let size = Math.min(100, this.limit - this.count) if (this[kFirst]) { @@ -174,15 +177,14 @@ class Iterator extends AbstractIterator { size = 1 } - this._nextv(size, emptyOptions, (err, entries) => { - if (err) return callback(err) - this[kCache] = entries - this._next(callback) - }) + this[kCache] = await this._nextv(size, emptyOptions) + + // Shift returns undefined if empty, which is what we want + return this[kCache].shift() } } - _all (options, callback) { + async _all (options) { this[kFirst] = false // TODO: mixing next and all is not covered by test suite @@ -190,14 +192,13 @@ class Iterator extends AbstractIterator { const size = this.limit - this.count - cache.length if (size <= 0) { - return this.nextTick(callback, null, cache) + return cache } - this._nextv(size, emptyOptions, (err, entries) => { - if (err) return callback(err) - if (cache.length > 0) entries = cache.concat(entries) - callback(null, entries) - }) + let entries = await this._nextv(size, emptyOptions) + if (cache.length > 0) entries = cache.concat(entries) + + return entries } _seek (target, options) { diff --git a/package.json b/package.json index f65a165..6516489 100644 --- a/package.json +++ b/package.json @@ -8,9 +8,7 @@ "types": "./index.d.ts", "scripts": { "test": "standard && hallmark && airtap --coverage test/index.js && nyc report", - "coverage": "nyc report -r lcovonly", - "dependency-check": "dependency-check --no-dev .", - "prepublishOnly": "npm run dependency-check" + "coverage": "nyc report -r lcovonly" }, "files": [ "index.js", @@ -21,16 +19,13 @@ "UPGRADING.md" ], "dependencies": { - "abstract-level": "^1.0.2", - "catering": "^2.1.1", - "module-error": "^1.0.2", - "run-parallel-limit": "^1.1.0" + "abstract-level": "^2.0.1", + "module-error": "^1.0.2" }, "devDependencies": { "@voxpelli/tsconfig": "^4.0.0", "airtap": "^5.0.0", "airtap-playwright": "^1.0.1", - "dependency-check": "^4.1.0", "hallmark": "^4.1.0", "nyc": "^15.0.0", "standard": "^17.0.0", diff --git a/test/custom-test.js b/test/custom-test.js index a53bc20..294d92f 100644 --- a/test/custom-test.js +++ b/test/custom-test.js @@ -3,123 +3,100 @@ const { BrowserLevel } = require('..') module.exports = function (test, testCommon) { - test('default namePrefix', function (t) { + test('default namePrefix', async function (t) { const db = testCommon.factory() t.ok(db.location, 'instance has location property') t.is(db.namePrefix, 'level-js-', 'instance has namePrefix property') - db.open(function (err) { - t.notOk(err, 'no open error') + await db.open() - const idb = db.db - const databaseName = idb.name - const storeNames = idb.objectStoreNames + const idb = db.db + const databaseName = idb.name + const storeNames = idb.objectStoreNames - t.is(databaseName, 'level-js-' + db.location, 'database name is prefixed') - t.is(storeNames.length, 1, 'created 1 object store') - t.is(storeNames.item(0), db.location, 'object store name equals location') + t.is(databaseName, 'level-js-' + db.location, 'database name is prefixed') + t.is(storeNames.length, 1, 'created 1 object store') + t.is(storeNames.item(0), db.location, 'object store name equals location') - db.close(t.end.bind(t)) - }) + return db.close() }) - test('custom namePrefix', function (t) { + test('custom namePrefix', async function (t) { const db = testCommon.factory({ prefix: 'custom-' }) t.ok(db.location, 'instance has location property') t.is(db.namePrefix, 'custom-', 'instance has namePrefix property') - db.open(function (err) { - t.notOk(err, 'no open error') + await db.open() - const idb = db.db - const databaseName = idb.name - const storeNames = idb.objectStoreNames + const idb = db.db + const databaseName = idb.name + const storeNames = idb.objectStoreNames - t.is(databaseName, 'custom-' + db.location, 'database name is prefixed') - t.is(storeNames.length, 1, 'created 1 object store') - t.is(storeNames.item(0), db.location, 'object store name equals location') + t.is(databaseName, 'custom-' + db.location, 'database name is prefixed') + t.is(storeNames.length, 1, 'created 1 object store') + t.is(storeNames.item(0), db.location, 'object store name equals location') - db.close(t.end.bind(t)) - }) + return db.close() }) - test('empty namePrefix', function (t) { + test('empty namePrefix', async function (t) { const db = testCommon.factory({ prefix: '' }) t.ok(db.location, 'instance has location property') t.is(db.namePrefix, '', 'instance has namePrefix property') - db.open(function (err) { - t.notOk(err, 'no open error') + await db.open() - const idb = db.db - const databaseName = idb.name - const storeNames = idb.objectStoreNames + const idb = db.db + const databaseName = idb.name + const storeNames = idb.objectStoreNames - t.is(databaseName, db.location, 'database name is prefixed') - t.is(storeNames.length, 1, 'created 1 object store') - t.is(storeNames.item(0), db.location, 'object store name equals location') + t.is(databaseName, db.location, 'database name is prefixed') + t.is(storeNames.length, 1, 'created 1 object store') + t.is(storeNames.item(0), db.location, 'object store name equals location') - db.close(t.end.bind(t)) - }) + return db.close() }) // NOTE: in chrome (at least) indexeddb gets buggy if you try and destroy a db, // then create it again, then try and destroy it again. these avoid doing that - test('test .destroy', function (t) { + test('test .destroy', async function (t) { const db = testCommon.factory() const location = db.location - db.open(function (err) { - t.notOk(err, 'no error') - db.put('key', 'value', function (err) { - t.notOk(err, 'no error') - db.get('key', function (err, value) { - t.notOk(err, 'no error') - t.equal(value, 'value', 'should have value') - db.close(function (err) { - t.notOk(err, 'no error') - BrowserLevel.destroy(location, function (err) { - t.notOk(err, 'no error') - const db2 = new BrowserLevel(location) - db2.get('key', function (err, value) { - t.is(err && err.code, 'LEVEL_NOT_FOUND', 'key is not there') - db2.close(t.end.bind(t)) - }) - }) - }) - }) - }) - }) + + await db.put('key', 'value') + + t.is(await db.get('key'), 'value', 'should have value') + + await db.close() + + await BrowserLevel.destroy(location) + const db2 = new BrowserLevel(location) + + t.is(await db2.get('key'), undefined, 'key is not there') + + return db2.close() }) - test('test .destroy and custom prefix', function (t) { + test('test .destroy and custom prefix', async function (t) { const prefix = 'custom-' const db = testCommon.factory({ prefix }) const location = db.location - db.open(function (err) { - t.notOk(err, 'no error') - db.put('key', 'value', function (err) { - t.notOk(err, 'no error') - db.get('key', function (err, value) { - t.notOk(err, 'no error') - t.equal(value, 'value', 'should have value') - db.close(function (err) { - t.notOk(err, 'no error') - BrowserLevel.destroy(location, prefix, function (err) { - t.notOk(err, 'no error') - const db2 = new BrowserLevel(location, { prefix }) - db2.get('key', function (err, value) { - t.is(err && err.code, 'LEVEL_NOT_FOUND', 'key is not there') - db2.close(t.end.bind(t)) - }) - }) - }) - }) - }) - }) + await db.put('key', 'value') + + t.is(await db.get('key'), 'value', 'should have value') + + await db.close() + + await BrowserLevel.destroy(location, prefix) + const db2 = new BrowserLevel(location, { prefix }) + + t.is(await db2.get('key'), undefined, 'key is not there') + + return db2.close() }) } diff --git a/util/clear.js b/util/clear.js index 48826bf..7a4a584 100644 --- a/util/clear.js +++ b/util/clear.js @@ -1,19 +1,20 @@ 'use strict' -module.exports = function clear (db, location, keyRange, options, callback) { - if (options.limit === 0) return db.nextTick(callback) +module.exports = async function clear (db, location, keyRange, options) { + if (options.limit === 0) return const transaction = db.db.transaction([location], 'readwrite') const store = transaction.objectStore(location) + let count = 0 - transaction.oncomplete = function () { - callback() - } + const promise = new Promise(function (resolve, reject) { + transaction.oncomplete = resolve - transaction.onabort = function () { - callback(transaction.error || new Error('aborted by user')) - } + transaction.onabort = function () { + reject(transaction.error || new Error('aborted by user')) + } + }) // A key cursor is faster (skips reading values) but not supported by IE // TODO: we no longer support IE. Test others @@ -32,4 +33,6 @@ module.exports = function clear (db, location, keyRange, options, callback) { } } } + + return promise } diff --git a/util/deserialize.js b/util/deserialize.js index 41dabee..5a66d1d 100644 --- a/util/deserialize.js +++ b/util/deserialize.js @@ -3,7 +3,10 @@ const textEncoder = new TextEncoder() module.exports = function (data) { - if (data instanceof Uint8Array) { + if (data === undefined) { + // Undefined means not found in both IndexedDB and AbstractLevel + return data + } else if (data instanceof Uint8Array) { return data } else if (data instanceof ArrayBuffer) { return new Uint8Array(data)