Skip to content

Commit eaa12e3

Browse files
authored
create a concrete wrapper for level (#259)
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`
1 parent aaaca05 commit eaa12e3

File tree

9 files changed

+212
-141
lines changed

9 files changed

+212
-141
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
# Decentralized Web Node (DWN) SDK
44

55
Code Coverage
6-
![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)
6+
![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)
77

88
## Introduction
99

src/store/blockstore-level.ts

Lines changed: 11 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
11
import type { AwaitIterable, Batch, KeyQuery, Pair, Query } from 'interface-store';
22
import type { Blockstore, Options } from 'interface-blockstore';
3-
import type { LevelDatabase, LevelDatabaseOptions } from './create-level.js';
43

5-
import { abortOr } from '../utils/abort.js';
64
import { CID } from 'multiformats';
7-
import { createLevelDatabase } from './create-level.js';
8-
import { sleep } from '../utils/time.js';
5+
import { createLevelDatabase, LevelWrapper } from './level-wrapper.js';
96

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

22-
db: LevelDatabase<string, Uint8Array>;
19+
db: LevelWrapper<Uint8Array>;
2320

24-
/**
25-
* @param location - must be a directory path (relative or absolute) where LevelDB will store its
26-
* files, or in browsers, the name of
27-
* the {@link https://developer.mozilla.org/en-US/docs/Web/API/IDBDatabase IDBDatabase}
28-
* to be opened.
29-
*/
30-
constructor(config: BlockstoreLevelConfig) {
21+
constructor(config: BlockstoreLevelConfig, db?: LevelWrapper<Uint8Array>) {
3122
this.config = {
3223
createLevelDatabase,
3324
...config
3425
};
26+
27+
this.db = db ?? new LevelWrapper<Uint8Array>({ ...this.config, valueEncoding: 'binary' });
3528
}
3629

3730
async open(): Promise<void> {
38-
await this.createLevelDatabase();
39-
40-
while (this.db.status === 'opening' || this.db.status === 'closing') {
41-
await sleep(200);
42-
}
43-
44-
if (this.db.status === 'open') {
45-
return;
46-
}
47-
48-
// db.open() is automatically called by the database constructor. We're calling it explicitly
49-
// in order to explicitly catch an error that would otherwise not surface
50-
// until another method like db.get() is called. Once open() has then been called,
51-
// any read & write operations will again be queued internally
52-
// until opening has finished.
5331
return this.db.open();
5432
}
5533

56-
/**
57-
* releases all file handles and locks held by the underlying db.
58-
*/
5934
async close(): Promise<void> {
60-
if (!this.db) {
61-
return;
62-
}
63-
64-
while (this.db.status === 'opening' || this.db.status === 'closing') {
65-
await sleep(200);
66-
}
67-
68-
if (this.db.status === 'closed') {
69-
return;
70-
}
71-
7235
return this.db.close();
7336
}
7437

7538
async partition(name: string): Promise<BlockstoreLevel> {
76-
await this.createLevelDatabase();
77-
78-
return new BlockstoreLevel({
79-
location : '',
80-
createLevelDatabase : async <K, V>(_location: string, options?: LevelDatabaseOptions<K, V>): Promise<LevelDatabase<K, V>> => {
81-
return this.db.sublevel(name, options);
82-
}
83-
});
39+
const db = await this.db.partition(name);
40+
return new BlockstoreLevel({ ...this.config, location: '' }, db);
8441
}
8542

8643
async put(key: CID, val: Uint8Array, options?: Options): Promise<void> {
87-
options?.signal?.throwIfAborted();
88-
89-
await abortOr(options?.signal, this.createLevelDatabase());
90-
91-
return abortOr(options?.signal, this.db.put(key.toString(), val));
44+
return this.db.put(key.toString(), val, options);
9245
}
9346

9447
async get(key: CID, options?: Options): Promise<Uint8Array> {
95-
options?.signal?.throwIfAborted();
96-
97-
await abortOr(options?.signal, this.createLevelDatabase());
98-
99-
try {
100-
const val = await abortOr(options?.signal, this.db.get(key.toString()));
101-
return val;
102-
} catch (e) {
103-
// level throws an error if the key is not present. Return undefined in this case
104-
if (e.code === 'LEVEL_NOT_FOUND') {
105-
return undefined;
106-
} else {
107-
throw e;
108-
}
109-
}
48+
return this.db.get(key.toString(), options);
11049
}
11150

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

11655
async delete(key: CID, options?: Options): Promise<void> {
117-
options?.signal?.throwIfAborted();
118-
119-
await abortOr(options?.signal, this.createLevelDatabase());
120-
121-
return abortOr(options?.signal, this.db.del(key.toString()));
56+
return this.db.delete(key.toString(), options);
12257
}
12358

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

148-
private async createLevelDatabase(): Promise<void> {
149-
this.db ??= await this.config.createLevelDatabase<string, Uint8Array>(this.config.location, { keyEncoding: 'utf8', valueEncoding: 'binary' });
150-
}
151-
15283
/**
15384
* deletes all entries
15485
*/
155-
clear(): Promise<void> {
86+
async clear(): Promise<void> {
15687
return this.db.clear();
15788
}
15889

src/store/create-level.ts

Lines changed: 0 additions & 12 deletions
This file was deleted.

src/store/data-store-level.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { PutResult } from './data-store.js';
33

44
import { BlockstoreLevel } from './blockstore-level.js';
55
import { CID } from 'multiformats/cid';
6-
import { createLevelDatabase } from './create-level.js';
6+
import { createLevelDatabase } from './level-wrapper.js';
77
import { DataStore } from './data-store.js';
88
import { exporter } from 'ipfs-unixfs-exporter';
99
import { importer } from 'ipfs-unixfs-importer';

src/store/index-level.ts

Lines changed: 15 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
1-
import type { LevelDatabase } from './create-level.js';
2-
import type { AbstractBatchDelOperation, AbstractBatchOperation, AbstractIteratorOptions } from 'abstract-level';
31
import type { Filter, RangeFilter } from '../core/types.js';
2+
import type { LevelWrapperBatchOperation, LevelWrapperIteratorOptions } from './level-wrapper.js';
43

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

97
export type Entry = {
108
_id: string,
@@ -21,38 +19,30 @@ export interface IndexLevelOptions {
2119
export class IndexLevel {
2220
config: IndexLevelConfig;
2321

24-
db: LevelDatabase<string, string>;
22+
db: LevelWrapper<string>;
2523

2624
constructor(config?: IndexLevelConfig) {
2725
this.config = {
2826
createLevelDatabase,
2927
...config
3028
};
29+
30+
this.db = new LevelWrapper<string>({ ...this.config, valueEncoding: 'utf8' });
3131
}
3232

3333
async open(): Promise<void> {
34-
await this.createLevelDatabase();
35-
3634
return this.db.open();
3735
}
3836

3937
async close(): Promise<void> {
40-
if (!this.db) {
41-
return;
42-
}
43-
4438
return this.db.close();
4539
}
4640

4741
async put(entry: Entry, options?: IndexLevelOptions): Promise<void> {
48-
options?.signal?.throwIfAborted();
49-
50-
await abortOr(options?.signal, this.createLevelDatabase());
51-
5242
entry = flatten(entry) as Entry;
5343
const { _id } = entry;
5444

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

70-
await abortOr(options?.signal, this.db.batch(ops));
60+
return this.db.batch(ops, options);
7161
}
7262

7363
async query(filter: Filter, options?: IndexLevelOptions): Promise<Array<string>> {
74-
options?.signal?.throwIfAborted();
75-
76-
await abortOr(options?.signal, this.createLevelDatabase());
77-
7864
const requiredProperties = new Set<string>();
7965
const missingPropertiesForID: { [id: string]: Set<string> } = { };
8066
const promises: Promise<Matches>[] = [ ];
@@ -116,32 +102,28 @@ export class IndexLevel {
116102
}
117103

118104
async delete(id: string, options?: IndexLevelOptions): Promise<void> {
119-
options?.signal?.throwIfAborted();
120-
121-
await abortOr(options?.signal, this.createLevelDatabase());
122-
123-
const prefixes = await abortOr(options?.signal, this.db.get(`__${id}__prefixes`));
105+
const prefixes = await this.db.get(`__${id}__prefixes`, options);
124106
if (!prefixes) {
125107
return;
126108
}
127109

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

134-
await abortOr(options?.signal, this.db.batch(ops));
116+
return this.db.batch(ops, options);
135117
}
136118

137-
clear(): Promise<void> {
119+
async clear(): Promise<void> {
138120
return this.db.clear();
139121
}
140122

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

144-
const iteratorOptions: AbstractIteratorOptions<string, string> = {
126+
const iteratorOptions: LevelWrapperIteratorOptions<string> = {
145127
gt: propertyKey
146128
};
147129

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

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

172154
private async findMatches(
173155
propertyName: string,
174-
iteratorOptions: AbstractIteratorOptions<string, string>,
156+
iteratorOptions: LevelWrapperIteratorOptions<string>,
175157
options?: IndexLevelOptions
176158
): Promise<Matches> {
177159
// 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.
@@ -181,9 +163,7 @@ export class IndexLevel {
181163
}
182164

183165
const matches = new Map<string, string>;
184-
for await (const [ key, value ] of this.db.iterator(iteratorOptions)) {
185-
options?.signal?.throwIfAborted();
186-
166+
for await (const [ key, value ] of this.db.iterator(iteratorOptions, options)) {
187167
if (!key.startsWith(propertyName)) {
188168
break;
189169
}
@@ -206,10 +186,6 @@ export class IndexLevel {
206186
private join(...values: unknown[]): string {
207187
return values.join(`\x00`);
208188
}
209-
210-
private async createLevelDatabase(): Promise<void> {
211-
this.db ??= await this.config.createLevelDatabase<string, string>(this.config.location, { keyEncoding: 'utf8', valueEncoding: 'utf8' });
212-
}
213189
}
214190

215191
type IndexLevelConfig = {

0 commit comments

Comments
 (0)