Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Breaking: upgrade to abstract-level 2 #16

Merged
merged 1 commit into from
Dec 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package-lock=false
19 changes: 2 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

<details><summary>Click to expand</summary>

- [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)

</details>

## Usage

```js
Expand Down Expand Up @@ -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`:

Expand Down
4 changes: 4 additions & 0 deletions UPGRADING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.**
Expand Down
3 changes: 0 additions & 3 deletions index.d.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import {
AbstractLevel,
AbstractDatabaseOptions,
NodeCallback,
AbstractOpenOptions,
AbstractGetOptions,
AbstractGetManyOptions,
Expand Down Expand Up @@ -58,8 +57,6 @@ export class BrowserLevel<KDefault = string, VDefault = string>
*/
static destroy (location: string): Promise<void>
static destroy (location: string, prefix: string): Promise<void>
static destroy (location: string, callback: NodeCallback<void>): void
static destroy (location: string, prefix: string, callback: NodeCallback<void>): void
}

/**
Expand Down
206 changes: 95 additions & 111 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -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, _) {
Expand Down Expand Up @@ -73,140 +70,139 @@ 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) {
const transaction = this[kIDB].transaction([this[kLocation]], mode)
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
_iterator (options) {
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 () {
Expand All @@ -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
Loading