Skip to content

Commit

Permalink
Breaking: upgrade to abstract-level 2 (#16)
Browse files Browse the repository at this point in the history
Category: change
Ref: #14
  • Loading branch information
vweevers authored Dec 1, 2024
1 parent a31ab82 commit 6296322
Show file tree
Hide file tree
Showing 10 changed files with 212 additions and 262 deletions.
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

0 comments on commit 6296322

Please sign in to comment.