Skip to content

Commit

Permalink
create a concrete wrapper for level (#259)
Browse files Browse the repository at this point in the history
the behavior adjustments made inside `BlockstoreLevel` (e.g. `get` not throwing if the key is not found) should be used everywhere so that we have a consistent pattern for using `level`
  • Loading branch information
dcrousso authored Mar 14, 2023
1 parent aaaca05 commit eaa12e3
Show file tree
Hide file tree
Showing 9 changed files with 212 additions and 141 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# Decentralized Web Node (DWN) SDK

Code Coverage
![Statements](https://img.shields.io/badge/statements-94.93%25-brightgreen.svg?style=flat) ![Branches](https://img.shields.io/badge/branches-92.92%25-brightgreen.svg?style=flat) ![Functions](https://img.shields.io/badge/functions-93.27%25-brightgreen.svg?style=flat) ![Lines](https://img.shields.io/badge/lines-94.93%25-brightgreen.svg?style=flat)
![Statements](https://img.shields.io/badge/statements-95.07%25-brightgreen.svg?style=flat) ![Branches](https://img.shields.io/badge/branches-92.99%25-brightgreen.svg?style=flat) ![Functions](https://img.shields.io/badge/functions-93.53%25-brightgreen.svg?style=flat) ![Lines](https://img.shields.io/badge/lines-95.07%25-brightgreen.svg?style=flat)

## Introduction

Expand Down
91 changes: 11 additions & 80 deletions src/store/blockstore-level.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
import type { AwaitIterable, Batch, KeyQuery, Pair, Query } from 'interface-store';
import type { Blockstore, Options } from 'interface-blockstore';
import type { LevelDatabase, LevelDatabaseOptions } from './create-level.js';

import { abortOr } from '../utils/abort.js';
import { CID } from 'multiformats';
import { createLevelDatabase } from './create-level.js';
import { sleep } from '../utils/time.js';
import { createLevelDatabase, LevelWrapper } from './level-wrapper.js';

// `level` works in Node.js 12+ and Electron 5+ on Linux, Mac OS, Windows and
// FreeBSD, including any future Node.js and Electron release thanks to Node-API, including ARM
Expand All @@ -19,106 +16,44 @@ import { sleep } from '../utils/time.js';
export class BlockstoreLevel implements Blockstore {
config: BlockstoreLevelConfig;

db: LevelDatabase<string, Uint8Array>;
db: LevelWrapper<Uint8Array>;

/**
* @param location - must be a directory path (relative or absolute) where LevelDB will store its
* files, or in browsers, the name of
* the {@link https://developer.mozilla.org/en-US/docs/Web/API/IDBDatabase IDBDatabase}
* to be opened.
*/
constructor(config: BlockstoreLevelConfig) {
constructor(config: BlockstoreLevelConfig, db?: LevelWrapper<Uint8Array>) {
this.config = {
createLevelDatabase,
...config
};

this.db = db ?? new LevelWrapper<Uint8Array>({ ...this.config, valueEncoding: 'binary' });
}

async open(): Promise<void> {
await this.createLevelDatabase();

while (this.db.status === 'opening' || this.db.status === 'closing') {
await sleep(200);
}

if (this.db.status === 'open') {
return;
}

// db.open() is automatically called by the database constructor. We're calling it explicitly
// in order to explicitly catch an error that would otherwise not surface
// until another method like db.get() is called. Once open() has then been called,
// any read & write operations will again be queued internally
// until opening has finished.
return this.db.open();
}

/**
* releases all file handles and locks held by the underlying db.
*/
async close(): Promise<void> {
if (!this.db) {
return;
}

while (this.db.status === 'opening' || this.db.status === 'closing') {
await sleep(200);
}

if (this.db.status === 'closed') {
return;
}

return this.db.close();
}

async partition(name: string): Promise<BlockstoreLevel> {
await this.createLevelDatabase();

return new BlockstoreLevel({
location : '',
createLevelDatabase : async <K, V>(_location: string, options?: LevelDatabaseOptions<K, V>): Promise<LevelDatabase<K, V>> => {
return this.db.sublevel(name, options);
}
});
const db = await this.db.partition(name);
return new BlockstoreLevel({ ...this.config, location: '' }, db);
}

async put(key: CID, val: Uint8Array, options?: Options): Promise<void> {
options?.signal?.throwIfAborted();

await abortOr(options?.signal, this.createLevelDatabase());

return abortOr(options?.signal, this.db.put(key.toString(), val));
return this.db.put(key.toString(), val, options);
}

async get(key: CID, options?: Options): Promise<Uint8Array> {
options?.signal?.throwIfAborted();

await abortOr(options?.signal, this.createLevelDatabase());

try {
const val = await abortOr(options?.signal, this.db.get(key.toString()));
return val;
} catch (e) {
// level throws an error if the key is not present. Return undefined in this case
if (e.code === 'LEVEL_NOT_FOUND') {
return undefined;
} else {
throw e;
}
}
return this.db.get(key.toString(), options);
}

async has(key: CID, options?: Options): Promise<boolean> {
return !! await this.get(key, options);
}

async delete(key: CID, options?: Options): Promise<void> {
options?.signal?.throwIfAborted();

await abortOr(options?.signal, this.createLevelDatabase());

return abortOr(options?.signal, this.db.del(key.toString()));
return this.db.delete(key.toString(), options);
}

async * putMany(source: AwaitIterable<Pair<CID, Uint8Array>>, options?: Options):
Expand All @@ -145,14 +80,10 @@ export class BlockstoreLevel implements Blockstore {
}
}

private async createLevelDatabase(): Promise<void> {
this.db ??= await this.config.createLevelDatabase<string, Uint8Array>(this.config.location, { keyEncoding: 'utf8', valueEncoding: 'binary' });
}

/**
* deletes all entries
*/
clear(): Promise<void> {
async clear(): Promise<void> {
return this.db.clear();
}

Expand Down
12 changes: 0 additions & 12 deletions src/store/create-level.ts

This file was deleted.

2 changes: 1 addition & 1 deletion src/store/data-store-level.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { PutResult } from './data-store.js';

import { BlockstoreLevel } from './blockstore-level.js';
import { CID } from 'multiformats/cid';
import { createLevelDatabase } from './create-level.js';
import { createLevelDatabase } from './level-wrapper.js';
import { DataStore } from './data-store.js';
import { exporter } from 'ipfs-unixfs-exporter';
import { importer } from 'ipfs-unixfs-importer';
Expand Down
54 changes: 15 additions & 39 deletions src/store/index-level.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import type { LevelDatabase } from './create-level.js';
import type { AbstractBatchDelOperation, AbstractBatchOperation, AbstractIteratorOptions } from 'abstract-level';
import type { Filter, RangeFilter } from '../core/types.js';
import type { LevelWrapperBatchOperation, LevelWrapperIteratorOptions } from './level-wrapper.js';

import { abortOr } from '../utils/abort.js';
import { createLevelDatabase } from './create-level.js';
import { flatten } from '../utils/object.js';
import { createLevelDatabase, LevelWrapper } from './level-wrapper.js';

export type Entry = {
_id: string,
Expand All @@ -21,38 +19,30 @@ export interface IndexLevelOptions {
export class IndexLevel {
config: IndexLevelConfig;

db: LevelDatabase<string, string>;
db: LevelWrapper<string>;

constructor(config?: IndexLevelConfig) {
this.config = {
createLevelDatabase,
...config
};

this.db = new LevelWrapper<string>({ ...this.config, valueEncoding: 'utf8' });
}

async open(): Promise<void> {
await this.createLevelDatabase();

return this.db.open();
}

async close(): Promise<void> {
if (!this.db) {
return;
}

return this.db.close();
}

async put(entry: Entry, options?: IndexLevelOptions): Promise<void> {
options?.signal?.throwIfAborted();

await abortOr(options?.signal, this.createLevelDatabase());

entry = flatten(entry) as Entry;
const { _id } = entry;

const ops: AbstractBatchOperation<LevelDatabase<string, string>, string, string>[] = [ ];
const ops: LevelWrapperBatchOperation<string>[] = [ ];
const prefixes: string[] = [ ];
for (const property in entry) {
if (property === '_id') {
Expand All @@ -67,14 +57,10 @@ export class IndexLevel {
}
ops.push({ type: 'put', key: `__${_id}__prefixes`, value: JSON.stringify(prefixes) });

await abortOr(options?.signal, this.db.batch(ops));
return this.db.batch(ops, options);
}

async query(filter: Filter, options?: IndexLevelOptions): Promise<Array<string>> {
options?.signal?.throwIfAborted();

await abortOr(options?.signal, this.createLevelDatabase());

const requiredProperties = new Set<string>();
const missingPropertiesForID: { [id: string]: Set<string> } = { };
const promises: Promise<Matches>[] = [ ];
Expand Down Expand Up @@ -116,32 +102,28 @@ export class IndexLevel {
}

async delete(id: string, options?: IndexLevelOptions): Promise<void> {
options?.signal?.throwIfAborted();

await abortOr(options?.signal, this.createLevelDatabase());

const prefixes = await abortOr(options?.signal, this.db.get(`__${id}__prefixes`));
const prefixes = await this.db.get(`__${id}__prefixes`, options);
if (!prefixes) {
return;
}

const ops: AbstractBatchDelOperation<LevelDatabase<string, string>, string>[] = [ ];
const ops: LevelWrapperBatchOperation<string>[] = [ ];
for (const prefix of JSON.parse(prefixes)) {
ops.push({ type: 'del', key: this.join(prefix, id) });
}
ops.push({ type: 'del', key: `__${id}__prefixes` });

await abortOr(options?.signal, this.db.batch(ops));
return this.db.batch(ops, options);
}

clear(): Promise<void> {
async clear(): Promise<void> {
return this.db.clear();
}

private async findExactMatches(propertyName: string, propertyValue: unknown, options?: IndexLevelOptions): Promise<Matches> {
const propertyKey = this.join(propertyName, this.encodeValue(propertyValue));

const iteratorOptions: AbstractIteratorOptions<string, string> = {
const iteratorOptions: LevelWrapperIteratorOptions<string> = {
gt: propertyKey
};

Expand All @@ -151,7 +133,7 @@ export class IndexLevel {
private async findRangeMatches(propertyName: string, range: RangeFilter, options?: IndexLevelOptions): Promise<Matches> {
const propertyKey = this.join(propertyName);

const iteratorOptions: AbstractIteratorOptions<string, string> = { };
const iteratorOptions: LevelWrapperIteratorOptions<string> = { };
for (const comparator in range) {
iteratorOptions[comparator] = this.join(propertyName, this.encodeValue(range[comparator]));
}
Expand All @@ -171,7 +153,7 @@ export class IndexLevel {

private async findMatches(
propertyName: string,
iteratorOptions: AbstractIteratorOptions<string, string>,
iteratorOptions: LevelWrapperIteratorOptions<string>,
options?: IndexLevelOptions
): Promise<Matches> {
// Since we will stop iterating if we encounter entries that do not start with the `propertyName`, we need to always start from the upper bound.
Expand All @@ -181,9 +163,7 @@ export class IndexLevel {
}

const matches = new Map<string, string>;
for await (const [ key, value ] of this.db.iterator(iteratorOptions)) {
options?.signal?.throwIfAborted();

for await (const [ key, value ] of this.db.iterator(iteratorOptions, options)) {
if (!key.startsWith(propertyName)) {
break;
}
Expand All @@ -206,10 +186,6 @@ export class IndexLevel {
private join(...values: unknown[]): string {
return values.join(`\x00`);
}

private async createLevelDatabase(): Promise<void> {
this.db ??= await this.config.createLevelDatabase<string, string>(this.config.location, { keyEncoding: 'utf8', valueEncoding: 'utf8' });
}
}

type IndexLevelConfig = {
Expand Down
Loading

0 comments on commit eaa12e3

Please sign in to comment.