From 8ea7888cda478099b04998d63790fc9799150344 Mon Sep 17 00:00:00 2001 From: Hugo Dias Date: Fri, 15 Jan 2021 16:09:17 +0000 Subject: [PATCH] feat: ts types, github ci and clean up (#56) - add ts types with jsdocs and aegir - remove travis and add github action - update deps and repo clean up (readme, package.json, etc.. ) Co-authored-by: Vasco Santos Co-authored-by: Irakli Gozalishvili Co-authored-by: achingbrain --- .github/workflows/main.yml | 67 +++++++++ .npmignore | 41 ----- .travis.yml | 40 ----- README.md | 298 +------------------------------------ package.json | 21 ++- src/adapter.js | 157 ++++++++++--------- src/errors.js | 43 ++++-- src/index.js | 12 +- src/key.js | 101 +++++++++---- src/memory.js | 36 ++++- src/tests.js | 54 ++++++- src/types.ts | 163 ++++++++++++++++++++ src/utils.js | 69 ++++++++- test/key.spec.js | 14 +- test/utils.spec.js | 19 ++- tsconfig.json | 10 ++ 16 files changed, 618 insertions(+), 527 deletions(-) create mode 100644 .github/workflows/main.yml delete mode 100644 .npmignore delete mode 100644 .travis.yml create mode 100644 src/types.ts create mode 100644 tsconfig.json diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..9e81b21 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,67 @@ +name: ci +on: + push: + branches: + - master + pull_request: + branches: + - master + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - run: npm install + - run: npx aegir lint + - uses: gozala/typescript-error-reporter-action@v1.0.8 + - run: npx aegir build + - run: npx aegir dep-check + - uses: ipfs/aegir/actions/bundle-size@master + name: size + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + test-node: + needs: check + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [windows-latest, ubuntu-latest, macos-latest] + node: [12, 14] + fail-fast: true + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node }} + - run: npm install + - run: npx nyc --reporter=lcov aegir test -t node -- --bail + - uses: codecov/codecov-action@v1 + test-chrome: + needs: check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - run: npm install + - run: npx aegir test -t browser -t webworker --bail + test-firefox: + needs: check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - run: npm install + - run: npx aegir test -t browser -t webworker --bail -- --browsers FirefoxHeadless + test-electron-main: + needs: check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - run: npm install + - run: npx xvfb-maybe aegir test -t electron-main --bail + test-electron-renderer: + needs: check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - run: npm install + - run: npx xvfb-maybe aegir test -t electron-renderer --bail \ No newline at end of file diff --git a/.npmignore b/.npmignore deleted file mode 100644 index d5b8420..0000000 --- a/.npmignore +++ /dev/null @@ -1,41 +0,0 @@ -yarn.lock -package-lock.json - -**/node_modules/ -**/*.log -test/repo-tests* - -# Logs -logs -*.log - -coverage -.nyc_output - -# Runtime data -pids -*.pid -*.seed - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage - -# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# node-waf configuration -.lock-wscript - -build - -# Dependency directory -# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git -node_modules - -.travis.yml -.github -docs -test \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index d0cf281..0000000 --- a/.travis.yml +++ /dev/null @@ -1,40 +0,0 @@ -language: node_js -cache: npm -stages: - - check - - test - - cov - -node_js: - - '10' - -os: - - linux - - osx - - windows - -script: npx nyc -s npm run test:node -- --bail -after_success: npx nyc report --reporter=text-lcov > coverage.lcov && npx codecov - -jobs: - include: - - stage: check - script: - - npx aegir commitlint --travis - - npx aegir dep-check - - npm run lint - - - stage: test - name: chrome - addons: - chrome: stable - script: npx aegir test -t browser -t webworker - - - stage: test - name: firefox - addons: - firefox: latest - script: npx aegir test -t browser -t webworker -- --browsers FirefoxHeadless - -notifications: - email: false diff --git a/README.md b/README.md index bf9bb93..c9bb7d1 100644 --- a/README.md +++ b/README.md @@ -3,13 +3,8 @@ [![](https://img.shields.io/badge/made%20by-Protocol%20Labs-blue.svg?style=flat-square)](http://ipn.io) [![](https://img.shields.io/badge/project-IPFS-blue.svg?style=flat-square)](http://ipfs.io/) [![](https://img.shields.io/badge/freenode-%23ipfs-blue.svg?style=flat-square)](http://webchat.freenode.net/?channels=%23ipfs) -[![standard-readme compliant](https://img.shields.io/badge/standard--readme-OK-green.svg?style=flat-square)](https://github.com/RichardLitt/standard-readme) -[![Build Status](https://flat.badgen.net/travis/ipfs/interface-datastore)](https://travis-ci.com/ipfs/interface-datastore) -[![Code Coverage](https://codecov.io/gh/ipfs/interface-datastore/branch/master/graph/badge.svg)](https://codecov.io/gh/ipfs/interface-datastore) -[![Dependency Status](https://david-dm.org/ipfs/interface-datastore.svg?style=flat-square)](https://david-dm.org/ipfs/interface-datastore) -[![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat-square)](https://github.com/feross/standard) -![](https://img.shields.io/badge/npm-%3E%3D3.0.0-orange.svg?style=flat-square) -![](https://img.shields.io/badge/Node.js-%3E%3D8.0.0-orange.svg?style=flat-square) +[![codecov](https://img.shields.io/codecov/c/github/ipfs/interface-datastore.svg?style=flat-square)](https://codecov.io/gh/ipfs/interface-datastore) +[![GitHub Workflow Status](https://img.shields.io/github/workflow/status/ipfs/interface-datastore/ci?label=ci&style=flat-square)](https://github.com/ipfs/interface-datastore/actions?query=branch%3Amaster+workflow%3Aci+) > Implementation of the [datastore](https://github.com/ipfs/go-datastore) interface in JavaScript @@ -25,41 +20,10 @@ - [Usage](#usage) - [Wrapping Stores](#wrapping-stores) - [Test suite](#test-suite) + - [Aborting requests](#aborting-requests) + - [Concurrency](#concurrency) - [Keys](#keys) - [API](#api) - - [`has(key, [options])` -> `Promise`](#haskey-options---promiseboolean) - - [Arguments](#arguments) - - [Example](#example) - - [`put(key, value, [options])` -> `Promise`](#putkey-value-options---promise) - - [Arguments](#arguments-1) - - [Example](#example-1) - - [`putMany(source, [options])` -> `AsyncIterator<{ key: Key, value: Uint8Array }>`](#putmanysource-options---asynciterator-key-key-value-uint8array-) - - [Arguments](#arguments-2) - - [Example](#example-2) - - [`get(key, [options])` -> `Promise`](#getkey-options---promiseuint8array) - - [Arguments](#arguments-3) - - [Example](#example-3) - - [`getMany(source, [options])` -> `AsyncIterator`](#getmanysource-options---asynciteratoruint8array) - - [Arguments](#arguments-4) - - [Example](#example-4) - - [`delete(key, [options])` -> `Promise`](#deletekey-options---promise) - - [Arguments](#arguments-5) - - [Example](#example-5) - - [`deleteMany(source, [options])` -> `AsyncIterator`](#deletemanysource-options---asynciteratorkey) - - [Arguments](#arguments-6) - - [Example](#example-6) - - [`query(query, [options])` -> `AsyncIterable`](#queryquery-options---asynciterableuint8array) - - [Arguments](#arguments-7) - - [Example](#example-7) - - [`batch()`](#batch) - - [Example](#example-8) - - [`put(key, value)`](#putkey-value) - - [`delete(key)`](#deletekey) - - [`commit([options])` -> `Promise`](#commitoptions---promisevoid) - - [Arguments](#arguments-8) - - [Example](#example-9) - - [`open()` -> `Promise`](#open---promise) - - [`close()` -> `Promise`](#close---promise) - [Contribute](#contribute) - [License](#license) @@ -121,12 +85,6 @@ See the [MemoryDatastore](./src/memory.js) for an example of how it is used. $ npm install interface-datastore ``` -The type definitions for this package are available on http://definitelytyped.org/. To install just use: - -```sh -$ npm install -D @types/interface-datastore -``` - ## Usage ### Wrapping Stores @@ -156,11 +114,11 @@ describe('mystore', () => { }) ``` -### Aborting requests +### Aborting requests Most API methods accept an [AbortSignal][] as part of an options object. Implementations may listen for an `abort` event emitted by this object, or test the `signal.aborted` property. When received implementations should tear down any long-lived requests or resources created. -### Concurrency +### Concurrency The streaming `(put|get|delete)Many` methods are intended to be used with modules such as [it-parallel-batch](https://www.npmjs.com/package/it-parallel-batch) to allow calling code to control levels of parallelisation. The batching method ensures results are returned in the correct order, but interface implementations should be thread safe. @@ -198,249 +156,7 @@ Also, every namespace can be parameterized to embed relevant object information. - `new Key('/Comedy/MontyPython/Sketch:CheeseShop/Character:Mousebender')` ## API - -Implementations of this interface should make the following methods available: - -### `has(key, [options])` -> `Promise` - -Check for the existence of a given key - -#### Arguments - -| Name | Type | Description | -| ---- | ---- | ----------- | -| key | [Key][] | The key to check the existance of | -| options | [Object][] | An options object, all properties are optional | -| options.signal | [AbortSignal][] | A way to signal that the caller is no longer interested in the outcome of this operation | - -#### Example - -```js -const exists = await store.has(new Key('awesome')) - -if (exists) { - console.log('it is there') -} else { - console.log('it is not there') -} -``` - -### `put(key, value, [options])` -> `Promise` - -Store a value with the given key. - -#### Arguments - -| Name | Type | Description | -| ---- | ---- | ----------- | -| key | [Key][] | The key to store the value under | -| value | [Uint8Array][] | Value to store | -| options | [Object][] | An options object, all properties are optional | -| options.signal | [AbortSignal][] | A way to signal that the caller is no longer interested in the outcome of this operation | - -#### Example - -```js -await store.put([{ key: new Key('awesome'), value: new Uint8Array([0, 1, 2, 3]) }]) -console.log('put content') -``` - -### `putMany(source, [options])` -> `AsyncIterator<{ key: Key, value: Uint8Array }>` - -Store many key-value pairs. - -#### Arguments - -| Name | Type | Description | -| ---- | ---- | ----------- | -| source | [AsyncIterator][]<{ key: [Key][], value: [Uint8Array][] }> | The key to store the value under | -| value | [Uint8Array][] | Value to store | -| options | [Object][] | An options object, all properties are optional | -| options.signal | [AbortSignal][] | A way to signal that the caller is no longer interested in the outcome of this operation | - -#### Example - -```js -const source = [{ key: new Key('awesome'), value: new Uint8Array([0, 1, 2, 3]) }] - -for await (const { key, value } of store.putMany(source)) { - console.info(`put content for key ${key}`) -} -``` - -### `get(key, [options])` -> `Promise` - -#### Arguments - -| Name | Type | Description | -| ---- | ---- | ----------- | -| key | [Key][] | The key retrieve the value for | -| options | [Object][] | An options object, all properties are optional | -| options.signal | [AbortSignal][] | A way to signal that the caller is no longer interested in the outcome of this operation | - -#### Example - -Retrieve the value stored under the given key. - -```js -const value = await store.get(new Key('awesome')) -console.log('got content: %s', value.toString('utf8')) -// => got content: datastore -``` - -### `getMany(source, [options])` -> `AsyncIterator` - -#### Arguments - -| Name | Type | Description | -| ---- | ---- | ----------- | -| source | [AsyncIterator][]<[Key][]> | One or more keys to retrieve values for | -| options | [Object][] | An options object, all properties are optional | -| options.signal | [AbortSignal][] | A way to signal that the caller is no longer interested in the outcome of this operation | - -#### Example - -Retrieve a stream of values stored under the given keys. - -```js -for await (const value of store.getMany([new Key('awesome')])) { - console.log('got content:', new TextDecoder('utf8').decode(value)) - // => got content: datastore -} -``` - -### `delete(key, [options])` -> `Promise` - -Delete the content stored under the given key. - -#### Arguments - -| Name | Type | Description | -| ---- | ---- | ----------- | -| key | [Key][] | The key to remove the value for | -| options | [Object][] | An options object, all properties are optional | -| options.signal | [AbortSignal][] | A way to signal that the caller is no longer interested in the outcome of this operation | - -#### Example - -```js -await store.delete(new Key('awesome')) -console.log('deleted awesome content :(') -``` - -### `deleteMany(source, [options])` -> `AsyncIterator` - -Delete the content stored under the given keys. - -#### Arguments - -| Name | Type | Description | -| ---- | ---- | ----------- | -| source | [AsyncIterator][]<[Key][]> | One or more keys to remove values for | -| options | [Object][] | An options object, all properties are optional | -| options.signal | [AbortSignal][] | A way to signal that the caller is no longer interested in the outcome of this operation | - -#### Example - -```js -const source = [new Key('awesome')] - -for await (const key of store.deleteMany(source)) { - console.log(`deleted content with key ${key}`) -} -``` - -### `query(query, [options])` -> `AsyncIterable` - -Search the store for some values. Returns an [AsyncIterable][] with each item being a [Uint8Array][]. - -#### Arguments - -| Name | Type | Description | -| ---- | ---- | ----------- | -| query | [Object][] | A query object, all properties are optional | -| query.prefix | [String][] | Only return values where the key starts with this prefix | -| query.filters | [Array][]<[Function][]([Uint8Array][]) -> [Boolean][]> | Filter the results according to the these functions | -| query.orders | [Array][]<[Function][]([Array][]<[Uint8Array][]>) -> [Array][]<[Uint8Array][]>> | Order the results according to these functions | -| query.limit | [Number][] | Only return this many records | -| query.offset | [Number][] | Skip this many records at the beginning | -| options | [Object][] | An options object, all properties are optional | -| options.signal | [AbortSignal][] | A way to signal that the caller is no longer interested in the outcome of this operation | - -#### Example - -```js -// retrieve __all__ values from the store -let list = [] -for await (const value of store.query({})) { - list.push(value) -} -console.log('ALL THE VALUES', list) -``` - -### `batch()` - -This will return an object with which you can chain multiple operations together, with them only being executed on calling `commit`. - -#### Example - -```js -const b = store.batch() - -for (let i = 0; i < 100; i++) { - b.put(new Key(`hello${i}`), new TextEncoder('utf8').encode(`hello world ${i}`)) -} - -await b.commit() -console.log('put 100 values') -``` - -#### `put(key, value)` - -Queue a put operation to the store. - -| Name | Type | Description | -| ---- | ---- | ----------- | -| key | [Key][] | The key to store the value under | -| value | [Uint8Array][] | Value to store | - -#### `delete(key)` - -Queue a delete operation to the store. - -| Name | Type | Description | -| ---- | ---- | ----------- | -| key | [Key][] | The key to remove the value for | - -#### `commit([options])` -> `Promise` - -Write all queued operations to the underyling store. The batch object should not be used after calling this. - -#### Arguments - -| Name | Type | Description | -| ---- | ---- | ----------- | -| options | [Object][] | An options object, all properties are optional | -| options.signal | [AbortSignal][] | A way to signal that the caller is no longer interested in the outcome of this operation | - -#### Example - -```js -const batch = store.batch() - -batch.put(new Key('to-put'), new TextEncoder('utf8').encode('hello world')) -batch.del(new Key('to-remove')) - -await batch.commit() -``` - -### `open()` -> `Promise` - -Opens the datastore, this is only needed if the store was closed before, otherwise this is taken care of by the constructor. - -### `close()` -> `Promise` - -Close the datastore, this should always be called to ensure resources are cleaned up. +https://ipfs.github.io/interface-datastore/ ## Contribute diff --git a/package.json b/package.json index f826bb6..3e363dc 100644 --- a/package.json +++ b/package.json @@ -4,9 +4,14 @@ "description": "datastore interface", "leadMaintainer": "Alex Potsides ", "main": "src/index.js", + "types": "dist/src/index.d.ts", + "files": [ + "src", + "dist" + ], "scripts": { + "prepare": "aegir build --no-bundle", "lint": "aegir lint", - "build": "aegir build", "test": "aegir test", "test:node": "aegir test --target node", "test:browser": "aegir test --target browser", @@ -33,19 +38,20 @@ }, "homepage": "https://github.com/ipfs/interface-datastore#readme", "devDependencies": { - "aegir": "^28.1.0", - "chai": "^4.1.2", - "dirty-chai": "^2.0.1" + "@types/err-code": "^2.0.0", + "aegir": "^30.3.0" }, "dependencies": { - "class-is": "^1.1.0", "err-code": "^2.0.1", - "ipfs-utils": "^4.0.1", + "ipfs-utils": "^6.0.0", "iso-random-stream": "^1.1.1", "it-all": "^1.0.2", "it-drain": "^1.0.1", "nanoid": "^3.0.2" }, + "eslintConfig": { + "extends": "ipfs" + }, "contributors": [ "achingbrain ", "David Dias ", @@ -63,6 +69,5 @@ "Hugo Dias ", "tcme ", "Adam Uhlir " - ], - "bundleDependencies": [] + ] } diff --git a/src/adapter.js b/src/adapter.js index 94c83e7..a36e095 100644 --- a/src/adapter.js +++ b/src/adapter.js @@ -3,92 +3,102 @@ const { filter, sortAll, take, map } = require('./utils') const drain = require('it-drain') -class InterfaceDatastoreAdapter { - async open () { // eslint-disable-line require-await - +/** + * @typedef {import('./key')} Key + * @typedef {import('./types').Pair} Pair + * @typedef {import('./types').Datastore} Datastore + * @typedef {import('./types').Options} Options + * @typedef {import('./types').Query} Query + * @typedef {import('./types').Batch} Batch + */ + +/** + * @template O + * @typedef {import('./types').AwaitIterable} AwaitIterable + */ + +/** + * @implements {Datastore} + */ +class Adapter { + /** + * @returns {Promise} + */ + open () { + return Promise.reject(new Error('.open is not implemented')) } - async close () { // eslint-disable-line require-await - + /** + * @returns {Promise} + */ + close () { + return Promise.reject(new Error('.close is not implemented')) } /** - * Store the passed value under the passed key - * * @param {Key} key * @param {Uint8Array} val - * @param {Object} options + * @param {Options} [options] * @returns {Promise} */ - async put (key, val, options = {}) { // eslint-disable-line require-await - + put (key, val, options) { + return Promise.reject(new Error('.put is not implemented')) } /** - * Store the given key/value pairs - * - * @param {AsyncIterator<{ key: Key, value: Uint8Array }>} source - * @param {Object} options - * @returns {AsyncIterator<{ key: Key, value: Uint8Array }>} + * @param {Key} key + * @param {Options} [options] + * @returns {Promise} */ - async * putMany (source, options = {}) { - for await (const { key, value } of source) { - await this.put(key, value, options) - yield { key, value } - } + get (key, options) { + return Promise.reject(new Error('.get is not implemented')) } /** - * Retrieve the value for the passed key - * * @param {Key} key - * @param {Object} options - * @returns {Promise} + * @param {Options} [options] + * @returns {Promise} */ - async get (key, options = {}) { // eslint-disable-line require-await - + has (key, options) { + return Promise.reject(new Error('.has is not implemented')) } /** - * Retrieve values for the passed keys - * - * @param {AsyncIterator} source - * @param {Object} options - * @returns {AsyncIterator} + * @param {Key} key + * @param {Options} [options] + * @returns {Promise} */ - async * getMany (source, options = {}) { - for await (const key of source) { - yield this.get(key, options) - } + delete (key, options) { + return Promise.reject(new Error('.delete is not implemented')) } /** - * Check for the existence of a value for the passed key - * - * @param {Key} key - * @returns {Promise} + * @param {AwaitIterable} source + * @param {Options} [options] + * @returns {AsyncIterable} */ - async has (key) { // eslint-disable-line require-await - + async * putMany (source, options = {}) { + for await (const { key, value } of source) { + await this.put(key, value, options) + yield { key, value } + } } /** - * Remove the record for the passed key - * - * @param {Key} key - * @param {Object} options - * @returns {Promise} + * @param {AwaitIterable} source + * @param {Options} [options] + * @returns {AsyncIterable} */ - async delete (key, options = {}) { // eslint-disable-line require-await - + async * getMany (source, options = {}) { + for await (const key of source) { + yield this.get(key, options) + } } /** - * Remove values for the passed keys - * - * @param {AsyncIterator} source - * @param {Object} options - * @returns {AsyncIterator} + * @param {AwaitIterable} source + * @param {Options} [options] + * @returns {AsyncIterable} */ async * deleteMany (source, options = {}) { for await (const key of source) { @@ -98,18 +108,19 @@ class InterfaceDatastoreAdapter { } /** - * Create a new batch object. - * - * @returns {Object} + * @returns {Batch} */ batch () { + /** @type {Pair[]} */ let puts = [] + /** @type {Key[]} */ let dels = [] return { put (key, value) { puts.push({ key, value }) }, + delete (key) { dels.push(key) }, @@ -123,28 +134,26 @@ class InterfaceDatastoreAdapter { } /** - * Yield all datastore values - * - * @param {Object} q - * @param {Object} options - * @returns {AsyncIterable<{ key: Key, value: Uint8Array }>} + * @param {Query} q + * @param {Options} [options] + * @returns {AsyncIterable} */ - async * _all (q, options) { // eslint-disable-line require-await - + // eslint-disable-next-line require-yield + async * _all (q, options) { + throw new Error('._all is not implemented') } /** - * Query the store. - * - * @param {Object} q - * @param {Object} options - * @returns {AsyncIterable} + * @param {Query} q + * @param {Options} [options] */ - async * query (q, options) { // eslint-disable-line require-await + query (q, options) { let it = this._all(q, options) if (q.prefix != null) { - it = filter(it, e => e.key.toString().startsWith(q.prefix)) + it = filter(it, (e) => + e.key.toString().startsWith(/** @type {string} */ (q.prefix)) + ) } if (Array.isArray(q.filters)) { @@ -157,7 +166,7 @@ class InterfaceDatastoreAdapter { if (q.offset != null) { let i = 0 - it = filter(it, () => i++ >= q.offset) + it = filter(it, () => i++ >= /** @type {number} */ (q.offset)) } if (q.limit != null) { @@ -165,11 +174,11 @@ class InterfaceDatastoreAdapter { } if (q.keysOnly === true) { - it = map(it, e => ({ key: e.key })) + return map(it, (e) => /** @type {Pair} */({ key: e.key })) } - yield * it + return it } } -module.exports = InterfaceDatastoreAdapter +module.exports = Adapter diff --git a/src/errors.js b/src/errors.js index 7e4941d..3fd3469 100644 --- a/src/errors.js +++ b/src/errors.js @@ -1,28 +1,51 @@ 'use strict' const errcode = require('err-code') - -module.exports.dbOpenFailedError = (err) => { +/** + * + * @param {Error} [err] + */ +const dbOpenFailedError = (err) => { err = err || new Error('Cannot open database') return errcode(err, 'ERR_DB_OPEN_FAILED') } - -module.exports.dbDeleteFailedError = (err) => { +/** + * + * @param {Error} [err] + */ +const dbDeleteFailedError = (err) => { err = err || new Error('Delete failed') return errcode(err, 'ERR_DB_DELETE_FAILED') } - -module.exports.dbWriteFailedError = (err) => { +/** + * + * @param {Error} [err] + */ +const dbWriteFailedError = (err) => { err = err || new Error('Write failed') return errcode(err, 'ERR_DB_WRITE_FAILED') } - -module.exports.notFoundError = (err) => { +/** + * + * @param {Error} [err] + */ +const notFoundError = (err) => { err = err || new Error('Not Found') return errcode(err, 'ERR_NOT_FOUND') } - -module.exports.abortedError = (err) => { +/** + * + * @param {Error} [err] + */ +const abortedError = (err) => { err = err || new Error('Aborted') return errcode(err, 'ERR_ABORTED') } + +module.exports = { + dbOpenFailedError, + dbDeleteFailedError, + dbWriteFailedError, + notFoundError, + abortedError +} diff --git a/src/index.js b/src/index.js index 2226059..742da51 100644 --- a/src/index.js +++ b/src/index.js @@ -6,8 +6,10 @@ const utils = require('./utils') const Errors = require('./errors') const Adapter = require('./adapter') -exports.Key = Key -exports.MemoryDatastore = MemoryDatastore -exports.utils = utils -exports.Errors = Errors -exports.Adapter = Adapter +module.exports = { + Key, + MemoryDatastore, + utils, + Errors, + Adapter +} diff --git a/src/key.js b/src/key.js index 181fc7d..9ebb86f 100644 --- a/src/key.js +++ b/src/key.js @@ -1,10 +1,10 @@ 'use strict' const { nanoid } = require('nanoid') -const withIs = require('class-is') const { utf8Encoder, utf8Decoder } = require('./utils') const TextDecoder = require('ipfs-utils/src/text-decoder') +const symbol = Symbol.for('@ipfs/interface-datastore/key') const pathSepS = '/' const pathSepB = utf8Encoder.encode(pathSepS) const pathSep = pathSepB[0] @@ -26,6 +26,10 @@ const pathSep = pathSepB[0] * */ class Key { + /** + * @param {string | Uint8Array} s + * @param {boolean} [clean] + */ constructor (s, clean) { if (typeof s === 'string') { this._buf = utf8Encoder.encode(s) @@ -51,7 +55,7 @@ class Key { /** * Convert to the string representation * - * @param {string} [encoding='utf8'] + * @param {string} [encoding='utf8'] - The encoding to use. * @returns {string} */ toString (encoding = 'utf8') { @@ -71,7 +75,13 @@ class Key { return this._buf } + get [symbol] () { + return true + } + /** + * Return string representation of the key + * * @returns {string} */ get [Symbol.toStringTag] () { @@ -81,16 +91,17 @@ class Key { /** * Constructs a key out of a namespace array. * - * @param {Array} list + * @param {Array} list - The array of namespaces * @returns {Key} * * @example + * ```js * Key.withNamespaces(['one', 'two']) * // => Key('/one/two') - * + * ``` */ static withNamespaces (list) { - return new _Key(list.join(pathSepS)) + return new Key(list.join(pathSepS)) } /** @@ -99,12 +110,13 @@ class Key { * @returns {Key} * * @example + * ```js * Key.random() * // => Key('/f98719ea086343f7b71f32ea9d9d521d') - * + * ``` */ static random () { - return new _Key(nanoid().replace(/-/g, '')) + return new Key(nanoid().replace(/-/g, '')) } /** @@ -133,7 +145,7 @@ class Key { /** * Check if the given key is sorted lower than ourself. * - * @param {Key} key + * @param {Key} key - The other Key to check against * @returns {boolean} */ less (key) { @@ -164,8 +176,10 @@ class Key { * @returns {Key} * * @example + * ```js * new Key('/Comedy/MontyPython/Actor:JohnCleese').reverse() * // => Key('/Actor:JohnCleese/MontyPython/Comedy') + * ``` */ reverse () { return Key.withNamespaces(this.list().slice().reverse()) @@ -185,9 +199,10 @@ class Key { * @returns {string} * * @example + * ```js * new Key('/Comedy/MontyPython/Actor:JohnCleese').baseNamespace() * // => 'Actor:JohnCleese' - * + * ``` */ baseNamespace () { const ns = this.namespaces() @@ -200,9 +215,10 @@ class Key { * @returns {Array} * * @example + * ```js * new Key('/Comedy/MontyPython/Actor:JohnCleese').list() * // => ['Comedy', 'MontyPythong', 'Actor:JohnCleese'] - * + * ``` */ list () { return this.toString().split(pathSepS).slice(1) @@ -214,9 +230,10 @@ class Key { * @returns {string} * * @example + * ```js * new Key('/Comedy/MontyPython/Actor:JohnCleese').type() * // => 'Actor' - * + * ``` */ type () { return namespaceType(this.baseNamespace()) @@ -228,8 +245,10 @@ class Key { * @returns {string} * * @example + * ```js * new Key('/Comedy/MontyPython/Actor:JohnCleese').name() * // => 'JohnCleese' + * ``` */ name () { return namespaceValue(this.baseNamespace()) @@ -238,15 +257,17 @@ class Key { /** * Returns an "instance" of this type key (appends value to namespace). * - * @param {string} s + * @param {string} s - The string to append. * @returns {Key} * * @example + * ```js * new Key('/Comedy/MontyPython/Actor').instance('JohnClesse') * // => Key('/Comedy/MontyPython/Actor:JohnCleese') + * ``` */ instance (s) { - return new _Key(this.toString() + ':' + s) + return new Key(this.toString() + ':' + s) } /** @@ -255,9 +276,10 @@ class Key { * @returns {Key} * * @example + * ```js * new Key('/Comedy/MontyPython/Actor:JohnCleese').path() * // => Key('/Comedy/MontyPython/Actor') - * + * ``` */ path () { let p = this.parent().toString() @@ -265,7 +287,7 @@ class Key { p += pathSepS } p += this.type() - return new _Key(p) + return new Key(p) } /** @@ -274,29 +296,31 @@ class Key { * @returns {Key} * * @example + * ```js * new Key("/Comedy/MontyPython/Actor:JohnCleese").parent() * // => Key("/Comedy/MontyPython") - * + * ``` */ parent () { const list = this.list() if (list.length === 1) { - return new _Key(pathSepS) + return new Key(pathSepS) } - return new _Key(list.slice(0, -1).join(pathSepS)) + return new Key(list.slice(0, -1).join(pathSepS)) } /** * Returns the `child` Key of this Key. * - * @param {Key} key + * @param {Key} key - The child Key to add * @returns {Key} * * @example + * ```js * new Key('/Comedy/MontyPython').child(new Key('Actor:JohnCleese')) * // => Key('/Comedy/MontyPython/Actor:JohnCleese') - * + * ``` */ child (key) { if (this.toString() === pathSepS) { @@ -305,19 +329,20 @@ class Key { return this } - return new _Key(this.toString() + key.toString(), false) + return new Key(this.toString() + key.toString(), false) } /** * Returns whether this key is a prefix of `other` * - * @param {Key} other + * @param {Key} other - The other key to test against * @returns {boolean} * * @example + * ```js * new Key('/Comedy').isAncestorOf('/Comedy/MontyPython') * // => true - * + * ``` */ isAncestorOf (other) { if (other.toString() === this.toString()) { @@ -330,13 +355,14 @@ class Key { /** * Returns whether this key is a contains another as prefix. * - * @param {Key} other + * @param {Key} other - The other Key to test against * @returns {boolean} * * @example + * ```js * new Key('/Comedy/MontyPython').isDecendantOf('/Comedy') * // => true - * + * ``` */ isDecendantOf (other) { if (other.toString() === this.toString()) { @@ -347,7 +373,7 @@ class Key { } /** - * Returns wether this key has only one namespace. + * Checks if this key has only one namespace. * * @returns {boolean} * @@ -359,12 +385,22 @@ class Key { /** * Concats one or more Keys into one new Key. * - * @param {Array} keys + * @param {Array} keys - The array of keys to concatenate * @returns {Key} */ concat (...keys) { return Key.withNamespaces([...this.namespaces(), ...flatten(keys.map(key => key.namespaces()))]) } + + /** + * Check if value is a Key instance + * + * @param {any} value - Value to check + * @returns {value is Key} + */ + static isKey (value) { + return value instanceof Key || Boolean(value && value[symbol]) + } } /** @@ -395,13 +431,12 @@ function namespaceValue (ns) { /** * Flatten array of arrays (only one level) * - * @param {Array} arr - * @returns {*} + * @template T + * @param {Array} arr + * @returns {T[]} */ function flatten (arr) { - return [].concat(...arr) + return /** @type {T[]} */([]).concat(...arr) } -const _Key = withIs(Key, { className: 'Key', symbolName: '@ipfs/interface-datastore/key' }) - -module.exports = _Key +module.exports = Key diff --git a/src/memory.js b/src/memory.js index eb659d3..81a8c71 100644 --- a/src/memory.js +++ b/src/memory.js @@ -2,36 +2,66 @@ const Key = require('./key') const Adapter = require('./adapter') - -// Errors const Errors = require('./errors') +/** + * @typedef {import('./types').Pair} Pair + * @typedef {import('./types').Datastore} Datastore + * @typedef {import('./types').Options} Options + */ + +/** + * @class MemoryDatastore + * @implements {Datastore} + */ class MemoryDatastore extends Adapter { constructor () { super() + /** @type {Record} */ this.data = {} } + open () { + return Promise.resolve() + } + + close () { + return Promise.resolve() + } + + /** + * @param {Key} key + * @param {Uint8Array} val + */ async put (key, val) { // eslint-disable-line require-await this.data[key.toString()] = val } + /** + * @param {Key} key + */ async get (key) { const exists = await this.has(key) if (!exists) throw Errors.notFoundError() return this.data[key.toString()] } + /** + * @param {Key} key + */ async has (key) { // eslint-disable-line require-await return this.data[key.toString()] !== undefined } + /** + * @param {Key} key + */ async delete (key) { // eslint-disable-line require-await delete this.data[key.toString()] } - * _all () { + async * _all () { yield * Object.entries(this.data) .map(([key, value]) => ({ key: new Key(key), value })) } diff --git a/src/tests.js b/src/tests.js index dab6fc0..d882105 100644 --- a/src/tests.js +++ b/src/tests.js @@ -1,24 +1,34 @@ /* eslint-env mocha */ -/* eslint max-nested-callbacks: ["error", 8] */ 'use strict' +// @ts-ignore const randomBytes = require('iso-random-stream/src/random') -const chai = require('chai') -chai.use(require('dirty-chai')) -const expect = chai.expect +const { expect } = require('aegir/utils/chai') const all = require('it-all') const drain = require('it-drain') const { utf8Encoder } = require('../src/utils') -const Key = require('../src').Key +const { Key } = require('../src') +/** + * @typedef {import('./types').Datastore} Datastore + * @typedef {import('./types').Pair} Pair + */ + +/** + * @param {{ teardown: () => void; setup: () => Datastore; }} test + */ module.exports = (test) => { + /** + * @param {Datastore} store + */ const cleanup = async store => { await store.close() await test.teardown() } describe('put', () => { + /** @type {Datastore} */ let store beforeEach(async () => { @@ -47,6 +57,7 @@ module.exports = (test) => { }) describe('putMany', () => { + /** @type {Datastore} */ let store beforeEach(async () => { @@ -77,6 +88,7 @@ module.exports = (test) => { }) describe('get', () => { + /** @type {Datastore} */ let store beforeEach(async () => { @@ -108,6 +120,7 @@ module.exports = (test) => { }) describe('getMany', () => { + /** @type {Datastore} */ let store beforeEach(async () => { @@ -142,6 +155,7 @@ module.exports = (test) => { }) describe('delete', () => { + /** @type {Datastore} */ let store beforeEach(async () => { @@ -161,6 +175,7 @@ module.exports = (test) => { }) it('parallel', async () => { + /** @type {[Key, Uint8Array][]} */ const data = [] for (let i = 0; i < 100; i++) { data.push([new Key(`/a/key${i}`), utf8Encoder.encode(`data${i}`)]) @@ -179,6 +194,7 @@ module.exports = (test) => { }) describe('deleteMany', () => { + /** @type {Datastore} */ let store beforeEach(async () => { @@ -214,6 +230,7 @@ module.exports = (test) => { }) describe('batch', () => { + /** @type {Datastore} */ let store beforeEach(async () => { @@ -252,9 +269,13 @@ module.exports = (test) => { await b.commit() + /** + * @param {AsyncIterable} iterable + */ const total = async iterable => { let count = 0 - for await (const _ of iterable) count++ // eslint-disable-line + // eslint-disable-next-line no-unused-vars + for await (const _ of iterable) count++ return count } @@ -265,14 +286,24 @@ module.exports = (test) => { }) describe('query', () => { + /** @type {Datastore} */ let store const hello = { key: new Key('/q/1hello'), value: utf8Encoder.encode('1') } const world = { key: new Key('/z/2world'), value: utf8Encoder.encode('2') } const hello2 = { key: new Key('/z/3hello2'), value: utf8Encoder.encode('3') } + /** + * @param {Pair} entry + */ const filter1 = entry => !entry.key.toString().endsWith('hello') + /** + * @param {Pair} entry + */ const filter2 = entry => entry.key.toString().endsWith('hello2') + /** + * @param {Pair[]} res + */ const order1 = res => { return res.sort((a, b) => { if (a.value.toString() < b.value.toString()) { @@ -282,6 +313,9 @@ module.exports = (test) => { }) } + /** + * @param {Pair[]} res + */ const order2 = res => { return res.sort((a, b) => { if (a.value.toString() < b.value.toString()) { @@ -294,6 +328,7 @@ module.exports = (test) => { }) } + /** @type {Array<[string, any, any[]|number]>} */ const tests = [ ['empty', {}, [hello, world, hello2]], ['prefix', { prefix: '/z' }, [world, hello2]], @@ -327,6 +362,10 @@ module.exports = (test) => { if (Array.isArray(expected)) { if (query.orders == null) { expect(res).to.have.length(expected.length) + /** + * @param {Pair} a + * @param {Pair} b + */ const s = (a, b) => { if (a.key.toString() < b.key.toString()) { return 1 @@ -358,7 +397,7 @@ module.exports = (test) => { const hello3 = { key: new Key('/z/4hello3'), value: utf8Encoder.encode('4') } let firstIteration = true - for await (const { key, value } of store.query({})) { // eslint-disable-line no-unused-vars + for await (const {} of store.query({})) { // eslint-disable-line no-empty-pattern if (firstIteration) { expect(await store.has(hello2.key)).to.be.true() await store.delete(hello2.key) @@ -388,6 +427,7 @@ module.exports = (test) => { }) describe('lifecycle', () => { + /** @type {Datastore} */ let store before(async () => { diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..d1b73bd --- /dev/null +++ b/src/types.ts @@ -0,0 +1,163 @@ +import type Key from './key' + +export type AwaitIterable = Iterable | AsyncIterable +export type Await = Promise | T +export interface Pair { + key: Key + value: Uint8Array +} +/** + * Options for async operations. + */ +export interface Options { + signal?: AbortSignal +} + +export interface Batch { + put: (key: Key, value: Uint8Array) => void + delete: (key: Key) => void + commit: (options?: Options) => Promise +} + +export interface DatastoreFactory extends Datastore { + new (): Datastore +} + +export interface Datastore { + open: () => Promise + close: () => Promise + /** + * Store the passed value under the passed key + * + * @example + * + * ```js + * await store.put([{ key: new Key('awesome'), value: new Uint8Array([0, 1, 2, 3]) }]) + * ``` + */ + put: (key: Key, val: Uint8Array, options?: Options) => Promise + /** + * Retrieve the value stored under the given key + * + * @example + * ```js + * const value = await store.get(new Key('awesome')) + * console.log('got content: %s', value.toString('utf8')) + * // => got content: datastore + * ``` + */ + get: (key: Key, options?: Options) => Promise + /** + * Check for the existence of a value for the passed key + * + * @example + * ```js + *const exists = await store.has(new Key('awesome')) + * + *if (exists) { + * console.log('it is there') + *} else { + * console.log('it is not there') + *} + *``` + */ + has: (key: Key, options?: Options) => Promise + /** + * Remove the record for the passed key + * + * @example + * + * ```js + * await store.delete(new Key('awesome')) + * console.log('deleted awesome content :(') + * ``` + */ + delete: (key: Key, options?: Options) => Promise + /** + * Store the given key/value pairs + * + * @example + * ```js + * const source = [{ key: new Key('awesome'), value: new Uint8Array([0, 1, 2, 3]) }] + * + * for await (const { key, value } of store.putMany(source)) { + * console.info(`put content for key ${key}`) + * } + * ``` + */ + putMany: ( + source: AwaitIterable, + options?: Options + ) => AsyncIterable + /** + * Retrieve values for the passed keys + * + * @example + * ```js + * for await (const value of store.getMany([new Key('awesome')])) { + * console.log('got content:', new TextDecoder('utf8').decode(value)) + * // => got content: datastore + * } + * ``` + */ + getMany: ( + source: AwaitIterable, + options?: Options + ) => AsyncIterable + /** + * Remove values for the passed keys + * + * @example + * + * ```js + * const source = [new Key('awesome')] + * + * for await (const key of store.deleteMany(source)) { + * console.log(`deleted content with key ${key}`) + * } + * ``` + */ + deleteMany: ( + source: AwaitIterable, + options?: Options + ) => AsyncIterable + /** + * This will return an object with which you can chain multiple operations together, with them only being executed on calling `commit`. + * + * @example + * ```js + * const b = store.batch() + * + * for (let i = 0; i < 100; i++) { + * b.put(new Key(`hello${i}`), new TextEncoder('utf8').encode(`hello world ${i}`)) + * } + * + * await b.commit() + * console.log('put 100 values') + * ``` + */ + batch: () => Batch + /** + * Query the store. + * + * @example + * ```js + * // retrieve __all__ values from the store + * let list = [] + * for await (const value of store.query({})) { + * list.push(value) + * } + * console.log('ALL THE VALUES', list) + * ``` + */ + query: (q: Query, options?: Options) => AsyncIterable +} + +export interface Query { + prefix?: string + filters?: Array<(item: Pair) => boolean> + orders?: Array<(items: Pair[]) => Await> + limit?: number + offset?: number + keysOnly?: boolean +} diff --git a/src/utils.js b/src/utils.js index 90c85fa..cb3445c 100644 --- a/src/utils.js +++ b/src/utils.js @@ -4,10 +4,28 @@ const tempdir = require('ipfs-utils/src/temp-dir') const TextEncoder = require('ipfs-utils/src/text-encoder') const TextDecoder = require('ipfs-utils/src/text-decoder') -exports.utf8Encoder = new TextEncoder('utf8') -exports.utf8Decoder = new TextDecoder('utf8') +/** + * @template T + * @typedef {import("./types").Await} PromiseOrValue + */ -exports.filter = (iterable, filterer) => { +/** + * @template T + * @typedef {import("./types").AwaitIterable} AnyIterable + */ + +const utf8Encoder = new TextEncoder() +const utf8Decoder = new TextDecoder('utf8') + +/** + * Filter + * + * @template T + * @param {AnyIterable} iterable + * @param {(item: T) => PromiseOrValue} filterer + * @returns {AsyncIterable} + */ +const filter = (iterable, filterer) => { return (async function * () { for await (const value of iterable) { const keep = await filterer(value) @@ -19,7 +37,15 @@ exports.filter = (iterable, filterer) => { // Not just sort, because the sorter is given all the values and should return // them all sorted -exports.sortAll = (iterable, sorter) => { +/** + * Sort All + * + * @template T + * @param {AnyIterable} iterable + * @param {(items: T[]) => PromiseOrValue} sorter + * @returns {AsyncIterable} + */ +const sortAll = (iterable, sorter) => { return (async function * () { let values = [] for await (const value of iterable) values.push(value) @@ -28,7 +54,14 @@ exports.sortAll = (iterable, sorter) => { })() } -exports.take = (iterable, n) => { +/** + * + * @template T + * @param {AsyncIterable | Iterable} iterable + * @param {number} n + * @returns {AsyncIterable} + */ +const take = (iterable, n) => { return (async function * () { if (n <= 0) return let i = 0 @@ -40,7 +73,14 @@ exports.take = (iterable, n) => { })() } -exports.map = (iterable, mapper) => { +/** + * + * @template T,O + * @param {AsyncIterable | Iterable} iterable + * @param {(item: T) => O} mapper + * @returns {AsyncIterable} + */ +const map = (iterable, mapper) => { return (async function * () { for await (const value of iterable) { yield mapper(value) @@ -48,9 +88,22 @@ exports.map = (iterable, mapper) => { })() } -exports.replaceStartWith = function (s, r) { +/** + * @param {string} s + * @param {string} r + */ +const replaceStartWith = (s, r) => { const matcher = new RegExp('^' + r) return s.replace(matcher, '') } -exports.tmpdir = tempdir +module.exports = { + map, + take, + sortAll, + filter, + utf8Encoder, + utf8Decoder, + tmpdir: tempdir, + replaceStartWith +} diff --git a/test/key.spec.js b/test/key.spec.js index 2bd2c89..59d78fe 100644 --- a/test/key.spec.js +++ b/test/key.spec.js @@ -1,13 +1,15 @@ /* eslint-env mocha */ 'use strict' -const expect = require('chai').expect - +const { expect } = require('aegir/utils/chai') const Key = require('../src').Key const pathSep = '/' describe('Key', () => { + /** + * @param {string} s + */ const clean = (s) => { let fixed = s if (fixed.startsWith(pathSep + pathSep)) { @@ -21,6 +23,9 @@ describe('Key', () => { } describe('basic', () => { + /** + * @param {string} s + */ const validKey = (s) => it(s, () => { const fixed = clean(pathSep + s) const namespaces = fixed.split(pathSep).slice(1) @@ -110,6 +115,7 @@ describe('Key', () => { }) it('random', () => { + /** @type {Record} */ const keys = {} const k = 100 for (let i = 0; i < k; i++) { @@ -122,6 +128,10 @@ describe('Key', () => { }) it('less', () => { + /** + * @param {string | Uint8Array} a + * @param {string | Uint8Array} b + */ const checkLess = (a, b) => { const ak = new Key(a) const bk = new Key(b) diff --git a/test/utils.spec.js b/test/utils.spec.js index f0449db..6f3bfe9 100644 --- a/test/utils.spec.js +++ b/test/utils.spec.js @@ -1,15 +1,15 @@ /* eslint-env mocha */ 'use strict' -const chai = require('chai') -chai.use(require('dirty-chai')) -const expect = chai.expect - +const { expect } = require('aegir/utils/chai') const utils = require('../src').utils describe('utils', () => { it('filter - sync', async () => { const data = [1, 2, 3, 4] + /** + * @param {number} val + */ const filterer = val => val % 2 === 0 const res = [] for await (const val of utils.filter(data, filterer)) { @@ -20,6 +20,9 @@ describe('utils', () => { it('filter - async', async () => { const data = [1, 2, 3, 4] + /** + * @param {number} val + */ const filterer = val => val % 2 === 0 const res = [] for await (const val of utils.filter(data, filterer)) { @@ -30,6 +33,9 @@ describe('utils', () => { it('sortAll', async () => { const data = [1, 2, 3, 4] + /** + * @param {number[]} vals + */ const sorter = vals => vals.reverse() const res = [] for await (const val of utils.sortAll(data, sorter)) { @@ -40,7 +46,7 @@ describe('utils', () => { it('sortAll - fail', async () => { const data = [1, 2, 3, 4] - const sorter = vals => { throw new Error('fail') } + const sorter = () => { throw new Error('fail') } const res = [] try { @@ -75,6 +81,9 @@ describe('utils', () => { it('should map iterator values', async () => { const data = [1, 2, 3, 4] + /** + * @param {number} n + */ const mapper = n => n * 2 const res = [] for await (const val of utils.map(data, mapper)) { diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..e605b61 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "./node_modules/aegir/src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": [ + "test", // remove this line if you don't want to type-check tests + "src" + ] +}