UNPKG

datastore-level

Version:

Datastore implementation with level(up|down) backend

325 lines (282 loc) 8.75 kB
/** * @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' import type { Batch, KeyQuery, Pair, Query } from 'interface-datastore' import type { AbortOptions } from 'interface-store' import type { DatabaseOptions, OpenOptions, IteratorOptions } from 'level' interface BatchPut { type: 'put' key: string value: Uint8Array } interface BatchDel { type: 'del' key: string } type BatchOp = BatchPut | BatchDel /** * A datastore backed by leveldb */ export class LevelDatastore extends BaseDatastore { public db: Level<string, Uint8Array> private readonly opts: OpenOptions constructor (path: string | Level<string, Uint8Array>, opts: DatabaseOptions<string, Uint8Array> & OpenOptions = {}) { 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 (): Promise<void> { try { await this.db.open(this.opts) } catch (err: any) { throw new OpenFailedError(String(err)) } } async put (key: Key, value: Uint8Array, options?: AbortOptions): Promise<Key> { try { options?.signal?.throwIfAborted() await raceSignal(this.db.put(key.toString(), value), options?.signal) return key } catch (err: any) { throw new PutFailedError(String(err)) } } async get (key: Key, options?: AbortOptions): Promise<Uint8Array> { let data try { options?.signal?.throwIfAborted() data = await raceSignal(this.db.get(key.toString()), options?.signal) } catch (err: any) { if (err.notFound != null) { throw new NotFoundError(String(err)) } throw new GetFailedError(String(err)) } return data } async has (key: Key, options?: AbortOptions): Promise<boolean> { try { options?.signal?.throwIfAborted() await raceSignal(this.db.get(key.toString()), options?.signal) } catch (err: any) { if (err.notFound != null) { return false } throw err } return true } async delete (key: Key, options?: AbortOptions): Promise<void> { try { options?.signal?.throwIfAborted() await raceSignal(this.db.del(key.toString()), options?.signal) } catch (err: any) { throw new DeleteFailedError(String(err)) } } async close (): Promise<void> { await this.db.close() } batch (): Batch { const ops: BatchOp[] = [] return { put: (key, value) => { ops.push({ type: 'put', key: key.toString(), value }) }, delete: (key) => { ops.push({ type: 'del', key: key.toString() }) }, commit: async (options?: AbortOptions) => { 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: Query, options?: AbortOptions): AsyncIterable<Pair> { 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: KeyQuery, options?: AbortOptions): AsyncIterable<Key> { 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: { values: boolean, prefix?: string }): AsyncIterable<Pair> { const iteratorOpts: IteratorOptions<string, Uint8Array> = { 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: AsyncIterable<[string, Uint8Array]> & { close(): Promise<void> }): AsyncIterable<Pair> { for await (const [key, value] of li) { yield { key: new Key(key, false), value } } await li.close() } interface OldLevelIterator { next(cb: (err: Error, key: string | Uint8Array | null, value: any) => void): void end(cb: (err: Error) => void): void } function oldLevelIteratorToIterator (li: OldLevelIterator): AsyncIterable<Pair> { 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 }) }) }) } } } }