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)