UNPKG

flash-store

Version:

FlashStore is a Key-Value persistent storage with easy to use ES6 Map-like API(both Async and Sync support), powered by LevelDB and TypeScript.

376 lines (321 loc) 9.62 kB
import * as path from 'path' import { path as appRoot, } from 'app-root-path' import rimraf from 'rimraf' // import encoding from 'encoding-down' // import leveldown from 'leveldown' // import levelup from 'levelup' import level from 'level' // https://github.com/rollup/rollup/issues/1267#issuecomment-296395734 // const rimraf = (<any>rimrafProxy).default || rimrafProxy // const encoding = (<any>encodingProxy).default || encodingProxy // // const leveldown = (<any>leveldownProxy).default || leveldownProxy // const levelup = (<any>levelupProxy).default || levelupProxy import { AsyncMapLike, } from 'async-map-like' import { log, VERSION, } from './config' export interface IteratorOptions { gt? : any, gte? : any, lt? : any, lte? : any, reverse? : boolean, limit? : number, prefix? : any, } export class FlashStore<K = any, V = any> implements AsyncMapLike<K, V> { static VERSION = VERSION private levelDb: level.LevelDB<K, V> /** * FlashStore is a Key-Value database tool and makes using leveldb more easy for Node.js * * Creates an instance of FlashStore. * @param {string} [workdir=path.join(appRoot, 'flash-store.workdir')] * @example * import { FlashStore } from 'flash-store' * const flashStore = new FlashStore('flashstore.workdir') */ constructor ( public workdir = path.join(appRoot, '.flash-store'), ) { log.verbose('FlashStore', 'constructor(%s)', workdir) // https://twitter.com/juliangruber/status/908688876381892608 // const encoded = encoding<K, V>( // leveldown(workdir), // { // // FIXME: issue #2 // valueEncoding: 'json', // }, // ) this.levelDb = level(workdir) // console.log((this.levelDb as any)._db.codec) this.levelDb.setMaxListeners(17) // default is Infinity } public version (): string { return VERSION } /** * Put data in database * * @param {K} key * @param {V} value * @returns {Promise<void>} * @example * await flashStore.put(1, 1) */ public async put (key: K, value: V): Promise<void> { log.warn('FlashStore', '`put()` DEPRECATED. use `set()` instead.') await this.set(key, value) } public async set (key: K, value: V): Promise<AsyncMapLike<K, V>> { log.verbose('FlashStore', 'set(%s, %s) value type: %s', key, value, typeof value) // FIXME: issue #2 await this.levelDb.put(key, JSON.stringify(value) as any) return this } /** * Get value from database by key * * @param {K} key * @returns {(Promise<V | null>)} * @example * console.log(await flashStore.get(1)) */ public async get (key: K): Promise<V | undefined> { log.verbose('FlashStore', 'get(%s)', key) try { // FIXME: issue #2 return JSON.parse(await this.levelDb.get(key) as any) } catch (e) { if (/^NotFoundError/.test(e)) { return undefined } throw e } } /** * Del data by key * * @param {K} key * @returns {Promise<void>} * @example * await flashStore.del(1) */ public async del (key: K): Promise<void> { log.verbose('FlashStore', '`del()` DEPRECATED. use `delete()` instead') await this.delete(key) } public async delete (key: K): Promise<boolean> { log.verbose('FlashStore', 'delete(%s)', key) await this.levelDb.del(key) // TODO: `del` returns `true` or `false` return true } /** * @typedef IteratorOptions * * @property { any } gt - Matches values that are greater than a specified value * @property { any } gte - Matches values that are greater than or equal to a specified value. * @property { any } lt - Matches values that are less than a specified value. * @property { any } lte - Matches values that are less than or equal to a specified value. * @property { boolean } reverse - Reverse the result set * @property { number } limit - Limits the number in the result set. * @property { any } prefix - Make the same prefix key get together. */ /** * Find keys by IteratorOptions * * @param {IteratorOptions} [options={}] * @returns {AsyncIterableIterator<K>} * @example * const flashStore = new FlashStore('flashstore.workdir') * for await(const key of flashStore.keys({gte: 1})) { * console.log(key) * } */ public async * keys (options: IteratorOptions = {}): AsyncIterableIterator<K> { log.verbose('FlashStore', 'keys()') // options = Object.assign(options, { // keys : true, // values : false, // }) if (options.prefix) { if (options.gte || options.lte) { throw new Error('can not specify `prefix` with `gte`/`lte` together.') } options.gte = options.prefix options.lte = options.prefix + '\xff' } for await (const [key] of this.entries(options)) { yield key } } /** * Find all values * * @returns {AsyncIterableIterator<V>} * @example * const flashStore = new FlashStore('flashstore.workdir') * for await(const value of flashStore.values()) { * console.log(value) * } */ public async * values (options: IteratorOptions = {}): AsyncIterableIterator<V> { log.verbose('FlashStore', 'values()') // options = Object.assign(options, { // keys : false, // values : true, // }) for await (const [, value] of this.entries(options)) { yield value } } /** * Get the counts of the database * @deprecated use property `size` instead * @returns {Promise<number>} * @example * const count = await flashStore.count() * console.log(`database count: ${count}`) */ public async count (): Promise<number> { log.warn('FlashStore', '`count()` DEPRECATED. use `size()` instead.') const size = await this.size return size } public get size (): Promise<number> { log.verbose('FlashStore', 'size()') /* eslint no-async-promise-executor: 0 */ // TODO: is there a better way to count all items from the db? return new Promise<number>(async (resolve, reject) => { try { let count = 0 for await (const _ of this) { count++ } resolve(count) } catch (e) { reject(e) } }) } /** * FIXME: use better way to do this */ public async has (key: K): Promise<boolean> { const val = await this.get(key) return !!val } /** * TODO: use better way to do this with leveldb */ public async clear (): Promise<void> { for await (const key of this.keys()) { await this.delete(key) } } get [Symbol.toStringTag] () { return Promise.resolve('FlashStore') } [Symbol.iterator] (): AsyncIterableIterator<[K, V]> { log.verbose('FlashStore', '[Symbol.iterator]()') /** * Huan(202108): what is this??? * does it equals to `entries()`? */ return this.entries() } /** * @private */ public async * entries (options?: IteratorOptions): AsyncIterableIterator<[K, V]> { log.verbose('FlashStore', '*entries(%s)', JSON.stringify(options)) const iterator = (this.levelDb as any).db.iterator(options) while (true) { const pair = await new Promise<[K, V] | null>((resolve, reject) => { iterator.next(function (err: any, key: K, val: V) { if (err) { reject(err) } if (!key && !val) { return resolve(null) } if (val) { // FIXME: issue #2 val = JSON.parse(val as any) } return resolve([key, val]) }) }) if (!pair) { break } yield pair } } public async * [Symbol.asyncIterator] (): AsyncIterableIterator<[K, V]> { log.verbose('FlashStore', '*[Symbol.asyncIterator]()') yield * this.entries() } /** * @private */ public async * streamAsyncIterator (): AsyncIterator<[K, V]> { log.warn('FlashStore', 'DEPRECATED *[Symbol.asyncIterator]()') const readStream = this.levelDb.createReadStream() const endPromise = new Promise<false>((resolve, reject) => { readStream .once('end', () => resolve(false)) .once('error', reject) }) let pair: [K, V] | false do { const dataPromise = new Promise<[K, V]>(resolve => { readStream.once('data', (data: any) => resolve([data.key, data.value])) }) pair = await Promise.race([ dataPromise, endPromise, ]) if (pair) { yield pair } } while (pair) } async forEach ( callbackfn: ( value: V, key: K, // map: TestAsyncMapLike, // FIXME(huan) 202007: we have to use any at here, because the typing system is very hard to // rename `Map` to `TestAsyncMapLike` in this method function parameters. map: any, ) => void, thisArg?: any, ): Promise<void> { log.verbose('FlashStore', 'forEach()') for await (const [key, value] of this) { callbackfn.call(thisArg, value, key, this) } } public async close (): Promise<void> { log.verbose('FlashStore', 'close()') await this.levelDb.close() } /** * Destroy the database * * @returns {Promise<void>} */ public async destroy (): Promise<void> { log.verbose('FlashStore', 'destroy()') await this.levelDb.close() await new Promise(resolve => rimraf(this.workdir, resolve)) } } export default FlashStore