datastore-level
Version:
Datastore implementation with level(up|down) backend
274 lines • 8.89 kB
JavaScript
/**
* @packageDocumentation
*
* A Datastore implementation that uses a flavour of [Level](https://leveljs.org/) as a backend.
*
* This module is targetted at Node.js. It is possible to use it in a browser but you should probably use IDBDatastore instead.
*
* @example
*
* ```js
* import { LevelDatastore } from 'datastore-level'
*
* // Default using level as backend for node or the browser
* const store = new LevelDatastore('path/to/store')
*
* // another leveldown compliant backend like memory-level
* const memStore = new LevelDatastore(
* new MemoryLevel({
* keyEncoding: 'utf8',
* valueEncoding: 'view'
* })
* )
* ```
*
* ## Browser Shimming Leveldown
*
* `LevelStore` uses the `level` module to automatically use `level` if a modern bundler is used which can detect bundle targets based on the `pkg.browser` property in your `package.json`.
*
* If you are using a bundler that does not support `pkg.browser`, you will need to handle the shimming yourself, as was the case with versions of `LevelStore` 0.7.0 and earlier.
*
* ## Database names
*
* `level-js@3` changed the database prefix from `IDBWrapper-` to `level-js-`, so please specify the old prefix if you wish to continue using databases created using `datastore-level` prior to `v0.12.0`. E.g.
*
* ```javascript
* import leveljs from 'level-js'
* import browserStore = new LevelDatastore(
* new Level('my/db/name', {
* prefix: 'IDBWrapper-'
* })
* })
* ```
*
* More information: [https://github.com/Level/level-js/blob/master/UPGRADING.md#new-database-prefix](https://github.com/Level/level-js/blob/99831913e905d19e5f6dee56d512b7264fbed7bd/UPGRADING.md#new-database-prefix)
*/
import { BaseDatastore } from 'datastore-core';
import { Key } from 'interface-datastore';
import { DeleteFailedError, GetFailedError, NotFoundError, OpenFailedError, PutFailedError } from 'interface-store';
import filter from 'it-filter';
import map from 'it-map';
import sort from 'it-sort';
import take from 'it-take';
import { Level } from 'level';
import { raceSignal } from 'race-signal';
/**
* A datastore backed by leveldb
*/
export class LevelDatastore extends BaseDatastore {
db;
opts;
constructor(path, opts = {}) {
super();
this.db = typeof path === 'string'
? new Level(path, {
...opts,
keyEncoding: 'utf8',
valueEncoding: 'view'
})
: path;
this.opts = {
createIfMissing: true,
compression: false, // same default as go
...opts
};
}
async open() {
try {
await this.db.open(this.opts);
}
catch (err) {
throw new OpenFailedError(String(err));
}
}
async put(key, value, options) {
try {
options?.signal?.throwIfAborted();
await raceSignal(this.db.put(key.toString(), value), options?.signal);
return key;
}
catch (err) {
throw new PutFailedError(String(err));
}
}
async get(key, options) {
let data;
try {
options?.signal?.throwIfAborted();
data = await raceSignal(this.db.get(key.toString()), options?.signal);
}
catch (err) {
if (err.notFound != null) {
throw new NotFoundError(String(err));
}
throw new GetFailedError(String(err));
}
return data;
}
async has(key, options) {
try {
options?.signal?.throwIfAborted();
await raceSignal(this.db.get(key.toString()), options?.signal);
}
catch (err) {
if (err.notFound != null) {
return false;
}
throw err;
}
return true;
}
async delete(key, options) {
try {
options?.signal?.throwIfAborted();
await raceSignal(this.db.del(key.toString()), options?.signal);
}
catch (err) {
throw new DeleteFailedError(String(err));
}
}
async close() {
await this.db.close();
}
batch() {
const ops = [];
return {
put: (key, value) => {
ops.push({
type: 'put',
key: key.toString(),
value
});
},
delete: (key) => {
ops.push({
type: 'del',
key: key.toString()
});
},
commit: async (options) => {
if (this.db.batch == null) {
throw new Error('Batch operations unsupported by underlying Level');
}
options?.signal?.throwIfAborted();
await raceSignal(this.db.batch(ops), options?.signal);
}
};
}
query(q, options) {
let it = map(this._query({
values: true,
prefix: q.prefix
}), (res) => {
options?.signal?.throwIfAborted();
return res;
});
if (Array.isArray(q.filters)) {
it = q.filters.reduce((it, f) => filter(it, f), it);
}
if (Array.isArray(q.orders)) {
it = q.orders.reduce((it, f) => sort(it, f), it);
}
const { offset, limit } = q;
if (offset != null) {
let i = 0;
it = filter(it, () => i++ >= offset);
}
if (limit != null) {
it = take(it, limit);
}
return it;
}
queryKeys(q, options) {
let it = map(this._query({
values: false,
prefix: q.prefix
}), ({ key }) => {
options?.signal?.throwIfAborted();
return key;
});
if (Array.isArray(q.filters)) {
it = q.filters.reduce((it, f) => filter(it, f), it);
}
if (Array.isArray(q.orders)) {
it = q.orders.reduce((it, f) => sort(it, f), it);
}
const { offset, limit } = q;
if (offset != null) {
let i = 0;
it = filter(it, () => i++ >= offset);
}
if (limit != null) {
it = take(it, limit);
}
return it;
}
_query(opts) {
const iteratorOpts = {
keys: true,
keyEncoding: 'buffer',
values: opts.values
};
// Let the db do the prefix matching
if (opts.prefix != null) {
const prefix = opts.prefix.toString();
// Match keys greater than or equal to `prefix` and
iteratorOpts.gte = prefix;
// less than `prefix` + \xFF (hex escape sequence)
iteratorOpts.lt = prefix + '\xFF';
}
const iterator = this.db.iterator(iteratorOpts);
if (iterator[Symbol.asyncIterator] != null) {
return levelIteratorToIterator(iterator);
}
// @ts-expect-error support older level
if (iterator.next != null && iterator.end != null) {
// @ts-expect-error support older level
return oldLevelIteratorToIterator(iterator);
}
throw new Error('Level returned incompatible iterator');
}
}
async function* levelIteratorToIterator(li) {
for await (const [key, value] of li) {
yield { key: new Key(key, false), value };
}
await li.close();
}
function oldLevelIteratorToIterator(li) {
return {
[Symbol.asyncIterator]() {
return {
next: async () => new Promise((resolve, reject) => {
li.next((err, key, value) => {
if (err != null) {
reject(err);
return;
}
if (key == null) {
li.end(err => {
if (err != null) {
reject(err);
return;
}
resolve({ done: true, value: undefined });
});
return;
}
resolve({ done: false, value: { key: new Key(key, false), value } });
});
}),
return: async () => new Promise((resolve, reject) => {
li.end(err => {
if (err != null) {
reject(err);
return;
}
resolve({ done: true, value: undefined });
});
})
};
}
};
}
//# sourceMappingURL=index.js.map