From 3919401e81daf0a41c10c55825ad552a42b91c3f Mon Sep 17 00:00:00 2001 From: achingbrain Date: Tue, 28 Jul 2020 14:11:21 +0100 Subject: [PATCH 1/3] fix: remove node buffer Swaps node Buffer for Uint8Array in keys and values. BREAKING CHANGE: node Buffers have been replaced with Uint8Arrays --- .npmignore | 19 +++++++++++++++---- README.md | 46 +++++++++++++++++++++++----------------------- package.json | 5 ++--- src/adapter.js | 14 +++++++------- src/key.js | 35 +++++++++++++++++++++-------------- src/tests.js | 38 +++++++++++++++++++------------------- src/utils.js | 5 +++++ 7 files changed, 92 insertions(+), 70 deletions(-) diff --git a/.npmignore b/.npmignore index 7eaff2d..d5b8420 100644 --- a/.npmignore +++ b/.npmignore @@ -1,7 +1,17 @@ +yarn.lock +package-lock.json + +**/node_modules/ +**/*.log +test/repo-tests* + # Logs logs *.log +coverage +.nyc_output + # Runtime data pids *.pid @@ -12,7 +22,6 @@ lib-cov # Coverage directory used by tools like istanbul coverage -.nyc_output # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) .grunt @@ -20,11 +29,13 @@ coverage # node-waf configuration .lock-wscript -# Compiled binary addons (http://nodejs.org/api/addons.html) -build/Release +build # Dependency directory # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git node_modules -test +.travis.yml +.github +docs +test \ No newline at end of file diff --git a/README.md b/README.md index 3cb3600..bf9bb93 100644 --- a/README.md +++ b/README.md @@ -33,13 +33,13 @@ - [`put(key, value, [options])` -> `Promise`](#putkey-value-options---promise) - [Arguments](#arguments-1) - [Example](#example-1) - - [`putMany(source, [options])` -> `AsyncIterator<{ key: Key, value: Buffer }>`](#putmanysource-options---asynciterator-key-key-value-buffer-) + - [`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---promisebuffer) + - [`get(key, [options])` -> `Promise`](#getkey-options---promiseuint8array) - [Arguments](#arguments-3) - [Example](#example-3) - - [`getMany(source, [options])` -> `AsyncIterator`](#getmanysource-options---asynciteratorbuffer) + - [`getMany(source, [options])` -> `AsyncIterator`](#getmanysource-options---asynciteratoruint8array) - [Arguments](#arguments-4) - [Example](#example-4) - [`delete(key, [options])` -> `Promise`](#deletekey-options---promise) @@ -48,7 +48,7 @@ - [`deleteMany(source, [options])` -> `AsyncIterator`](#deletemanysource-options---asynciteratorkey) - [Arguments](#arguments-6) - [Example](#example-6) - - [`query(query, [options])` -> `AsyncIterable`](#queryquery-options---asynciterablebuffer) + - [`query(query, [options])` -> `AsyncIterable`](#queryquery-options---asynciterableuint8array) - [Arguments](#arguments-7) - [Example](#example-7) - [`batch()`](#batch) @@ -179,11 +179,11 @@ for await (const { key, data } of batch(store.putMany(source), 10)) { ### Keys -To allow a better abstraction on how to address values, there is a `Key` class which is used as identifier. It's easy to create a key from a `Buffer` or a `string`. +To allow a better abstraction on how to address values, there is a `Key` class which is used as identifier. It's easy to create a key from a `Uint8Array` or a `string`. ```js const a = new Key('a') -const b = new Key(Buffer.from('hello')) +const b = new Key(new Uint8Array([0, 1, 2, 3])) ``` The key scheme is inspired by file systems and Google App Engine key model. Keys are meant to be unique across a system. They are typically hierarchical, incorporating more and more specific namespaces. Thus keys can be deemed 'children' or 'ancestors' of other keys: @@ -234,18 +234,18 @@ Store a value with the given key. | Name | Type | Description | | ---- | ---- | ----------- | | key | [Key][] | The key to store the value under | -| value | [Buffer][] | Value to store | +| 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: Buffer.from('datastores') }]) +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: Buffer }>` +### `putMany(source, [options])` -> `AsyncIterator<{ key: Key, value: Uint8Array }>` Store many key-value pairs. @@ -253,22 +253,22 @@ Store many key-value pairs. | Name | Type | Description | | ---- | ---- | ----------- | -| source | [AsyncIterator][]<{ key: [Key][], value: [Buffer][] }> | The key to store the value under | -| value | [Buffer][] | Value to store | +| 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: Buffer.from('datastores') }] +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` +### `get(key, [options])` -> `Promise` #### Arguments @@ -288,7 +288,7 @@ console.log('got content: %s', value.toString('utf8')) // => got content: datastore ``` -### `getMany(source, [options])` -> `AsyncIterator` +### `getMany(source, [options])` -> `AsyncIterator` #### Arguments @@ -304,7 +304,7 @@ 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: %s', value.toString('utf8')) + console.log('got content:', new TextDecoder('utf8').decode(value)) // => got content: datastore } ``` @@ -350,9 +350,9 @@ for await (const key of store.deleteMany(source)) { } ``` -### `query(query, [options])` -> `AsyncIterable` +### `query(query, [options])` -> `AsyncIterable` -Search the store for some values. Returns an [AsyncIterable][] with each item being a [Buffer][]. +Search the store for some values. Returns an [AsyncIterable][] with each item being a [Uint8Array][]. #### Arguments @@ -360,8 +360,8 @@ Search the store for some values. Returns an [AsyncIterable][] with each item be | ---- | ---- | ----------- | | 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][]([Buffer][]) -> [Boolean][]> | Filter the results according to the these functions | -| query.orders | [Array][]<[Function][]([Array][]<[Buffer][]>) -> [Array][]<[Buffer][]>> | Order the results according to these functions | +| 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 | @@ -388,7 +388,7 @@ This will return an object with which you can chain multiple operations together const b = store.batch() for (let i = 0; i < 100; i++) { - b.put(new Key(`hello${i}`), Buffer.from(`hello world ${i}`)) + b.put(new Key(`hello${i}`), new TextEncoder('utf8').encode(`hello world ${i}`)) } await b.commit() @@ -402,7 +402,7 @@ Queue a put operation to the store. | Name | Type | Description | | ---- | ---- | ----------- | | key | [Key][] | The key to store the value under | -| value | [Buffer][] | Value to store | +| value | [Uint8Array][] | Value to store | #### `delete(key)` @@ -428,7 +428,7 @@ Write all queued operations to the underyling store. The batch object should not ```js const batch = store.batch() -batch.put(new Key('to-put'), Buffer.from('hello world')) +batch.put(new Key('to-put'), new TextEncoder('utf8').encode('hello world')) batch.del(new Key('to-remove')) await batch.commit() @@ -455,7 +455,7 @@ MIT 2017 © IPFS [Key]: #Keys [Object]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object -[Buffer]: https://nodejs.org/api/buffer.html +[Uint8Array]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array [AbortSignal]: https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal [AsyncIterator]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/asyncIterator [AsyncIterable]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols diff --git a/package.json b/package.json index d350dab..c41a0da 100644 --- a/package.json +++ b/package.json @@ -33,15 +33,14 @@ }, "homepage": "https://github.com/ipfs/interface-datastore#readme", "devDependencies": { - "aegir": "^22.0.0", + "aegir": "^25.0.0", "chai": "^4.1.2", "dirty-chai": "^2.0.1" }, "dependencies": { - "buffer": "^5.5.0", "class-is": "^1.1.0", "err-code": "^2.0.1", - "ipfs-utils": "^2.2.2", + "ipfs-utils": "^2.3.1", "iso-random-stream": "^1.1.1", "it-all": "^1.0.2", "it-drain": "^1.0.1", diff --git a/src/adapter.js b/src/adapter.js index 42be9e5..12d061c 100644 --- a/src/adapter.js +++ b/src/adapter.js @@ -16,7 +16,7 @@ class InterfaceDatastoreAdapter { * Store the passed value under the passed key * * @param {Key} key - * @param {Buffer} val + * @param {Uint8Array} val * @param {Object} options * @returns {Promise} */ @@ -27,9 +27,9 @@ class InterfaceDatastoreAdapter { /** * Store the given key/value pairs * - * @param {AsyncIterator<{ key: Key, value: Buffer }>} source + * @param {AsyncIterator<{ key: Key, value: Uint8Array }>} source * @param {Object} options - * @returns {AsyncIterator<{ key: Key, value: Buffer }>} + * @returns {AsyncIterator<{ key: Key, value: Uint8Array }>} */ async * putMany (source, options = {}) { for await (const { key, value } of source) { @@ -43,7 +43,7 @@ class InterfaceDatastoreAdapter { * * @param {Key} key * @param {Object} options - * @returns {Promise} + * @returns {Promise} */ async get (key, options = {}) { // eslint-disable-line require-await @@ -54,7 +54,7 @@ class InterfaceDatastoreAdapter { * * @param {AsyncIterator} source * @param {Object} options - * @returns {AsyncIterator} + * @returns {AsyncIterator} */ async * getMany (source, options = {}) { for await (const key of source) { @@ -127,7 +127,7 @@ class InterfaceDatastoreAdapter { * * @param {Object} q * @param {Object} options - * @returns {AsyncIterable<{ key: Key, value: Buffer }>} + * @returns {AsyncIterable<{ key: Key, value: Uint8Array }>} */ async * _all (q, options) { // eslint-disable-line require-await @@ -138,7 +138,7 @@ class InterfaceDatastoreAdapter { * * @param {Object} q * @param {Object} options - * @returns {AsyncIterable} + * @returns {AsyncIterable} */ async * query (q, options) { // eslint-disable-line require-await let it = this._all(q, options) diff --git a/src/key.js b/src/key.js index 3513ac2..94dac39 100644 --- a/src/key.js +++ b/src/key.js @@ -1,11 +1,12 @@ 'use strict' -const { Buffer } = require('buffer') const { nanoid } = require('nanoid') const withIs = require('class-is') +const { utf8Encoder, utf8Decoder } = require('./utils') +const TextDecoder = require('ipfs-utils/src/text-decoder') const pathSepS = '/' -const pathSepB = Buffer.from(pathSepS) +const pathSepB = utf8Encoder.encode(pathSepS) const pathSep = pathSepB[0] /** @@ -27,9 +28,11 @@ const pathSep = pathSepB[0] class Key { constructor (s, clean) { if (typeof s === 'string') { - this._buf = Buffer.from(s) - } else if (Buffer.isBuffer(s)) { + this._buf = utf8Encoder.encode(s) + } else if (s instanceof Uint8Array) { this._buf = s + } else { + throw new Error('Invalid key, should be String of Uint8Array') } if (clean == null) { @@ -40,7 +43,7 @@ class Key { this.clean() } - if (this._buf.length === 0 || this._buf[0] !== pathSep) { + if (this._buf.byteLength === 0 || this._buf[0] !== pathSep) { throw new Error('Invalid key') } } @@ -51,14 +54,18 @@ class Key { * @param {string} [encoding='utf8'] * @returns {string} */ - toString (encoding) { - return this._buf.toString(encoding || 'utf8') + toString (encoding = 'utf8') { + if (encoding === 'utf8' || encoding === 'utf-8') { + return utf8Decoder.decode(this._buf) + } + + return new TextDecoder(encoding).decode(this._buf) } /** - * Return the buffer representation of the key + * Return the Uint8Array representation of the key * - * @returns {Buffer} + * @returns {Uint8Array} */ toBuffer () { return this._buf @@ -106,17 +113,17 @@ class Key { * @returns {void} */ clean () { - if (!this._buf || this._buf.length === 0) { - this._buf = Buffer.from(pathSepS) + if (!this._buf || this._buf.byteLength === 0) { + this._buf = pathSepB } if (this._buf[0] !== pathSep) { - this._buf = Buffer.concat([pathSepB, this._buf]) + this._buf = Uint8Array.of(pathSep, ...this._buf) } // normalize does not remove trailing slashes - while (this._buf.length > 1 && this._buf[this._buf.length - 1] === pathSep) { - this._buf = this._buf.slice(0, -1) + while (this._buf.byteLength > 1 && this._buf[this._buf.byteLength - 1] === pathSep) { + this._buf = Uint8Array.of(...this._buf.slice(0, -1)) } } diff --git a/src/tests.js b/src/tests.js index da3897a..131e14c 100644 --- a/src/tests.js +++ b/src/tests.js @@ -2,13 +2,13 @@ /* eslint max-nested-callbacks: ["error", 8] */ 'use strict' -const { Buffer } = require('buffer') const randomBytes = require('iso-random-stream/src/random') const chai = require('chai') chai.use(require('dirty-chai')) const expect = chai.expect const all = require('it-all') const drain = require('it-drain') +const { utf8Encoder } = require('../src/utils') const Key = require('../src').Key @@ -30,13 +30,13 @@ module.exports = (test) => { it('simple', () => { const k = new Key('/z/one') - return store.put(k, Buffer.from('one')) + return store.put(k, utf8Encoder.encode('one')) }) it('parallel', async () => { const data = [] for (let i = 0; i < 100; i++) { - data.push({ key: new Key(`/z/key${i}`), value: Buffer.from(`data${i}`) }) + data.push({ key: new Key(`/z/key${i}`), value: utf8Encoder.encode(`data${i}`) }) } await Promise.all(data.map(d => store.put(d.key, d.value))) @@ -59,7 +59,7 @@ module.exports = (test) => { it('streaming', async () => { const data = [] for (let i = 0; i < 100; i++) { - data.push({ key: new Key(`/z/key${i}`), value: Buffer.from(`data${i}`) }) + data.push({ key: new Key(`/z/key${i}`), value: utf8Encoder.encode(`data${i}`) }) } let index = 0 @@ -88,9 +88,9 @@ module.exports = (test) => { it('simple', async () => { const k = new Key('/z/one') - await store.put(k, Buffer.from('hello')) + await store.put(k, utf8Encoder.encode('hello')) const res = await store.get(k) - expect(res).to.be.eql(Buffer.from('hello')) + expect(res).to.be.eql(utf8Encoder.encode('hello')) }) it('should throw error for missing key', async () => { @@ -119,12 +119,12 @@ module.exports = (test) => { it('streaming', async () => { const k = new Key('/z/one') - await store.put(k, Buffer.from('hello')) + await store.put(k, utf8Encoder.encode('hello')) const source = [k] const res = await all(store.getMany(source)) expect(res).to.have.lengthOf(1) - expect(res[0]).to.be.eql(Buffer.from('hello')) + expect(res[0]).to.be.eql(utf8Encoder.encode('hello')) }) it('should throw error for missing key', async () => { @@ -153,7 +153,7 @@ module.exports = (test) => { it('simple', async () => { const k = new Key('/z/one') - await store.put(k, Buffer.from('hello')) + await store.put(k, utf8Encoder.encode('hello')) await store.get(k) await store.delete(k) const exists = await store.has(k) @@ -163,7 +163,7 @@ module.exports = (test) => { it('parallel', async () => { const data = [] for (let i = 0; i < 100; i++) { - data.push([new Key(`/a/key${i}`), Buffer.from(`data${i}`)]) + data.push([new Key(`/a/key${i}`), utf8Encoder.encode(`data${i}`)]) } await Promise.all(data.map(d => store.put(d[0], d[1]))) @@ -191,7 +191,7 @@ module.exports = (test) => { it('streaming', async () => { const data = [] for (let i = 0; i < 100; i++) { - data.push({ key: new Key(`/a/key${i}`), value: Buffer.from(`data${i}`) }) + data.push({ key: new Key(`/a/key${i}`), value: utf8Encoder.encode(`data${i}`) }) } await drain(store.putMany(data)) @@ -226,11 +226,11 @@ module.exports = (test) => { it('simple', async () => { const b = store.batch() - await store.put(new Key('/z/old'), Buffer.from('old')) + await store.put(new Key('/z/old'), utf8Encoder.encode('old')) - b.put(new Key('/a/one'), Buffer.from('1')) - b.put(new Key('/q/two'), Buffer.from('2')) - b.put(new Key('/q/three'), Buffer.from('3')) + b.put(new Key('/a/one'), utf8Encoder.encode('1')) + b.put(new Key('/q/two'), utf8Encoder.encode('2')) + b.put(new Key('/q/three'), utf8Encoder.encode('3')) b.delete(new Key('/z/old')) await b.commit() @@ -266,9 +266,9 @@ module.exports = (test) => { describe('query', () => { let store - const hello = { key: new Key('/q/1hello'), value: Buffer.from('1') } - const world = { key: new Key('/z/2world'), value: Buffer.from('2') } - const hello2 = { key: new Key('/z/3hello2'), value: Buffer.from('3') } + 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') } const filter1 = entry => !entry.key.toString().endsWith('hello') const filter2 = entry => entry.key.toString().endsWith('hello2') @@ -343,7 +343,7 @@ module.exports = (test) => { if (r.value == null) { expect(exp[i].value).to.not.exist() } else { - expect(r.value.equals(exp[i].value)).to.be.eql(true) + expect(r.value).to.deep.equal(exp[i].value) } }) } else { diff --git a/src/utils.js b/src/utils.js index ea5459a..90c85fa 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,6 +1,11 @@ 'use strict' 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') exports.filter = (iterable, filterer) => { return (async function * () { From 89aafe34b147d2be7a9e5a5373f3e5633081f999 Mon Sep 17 00:00:00 2001 From: achingbrain Date: Tue, 28 Jul 2020 14:25:52 +0100 Subject: [PATCH 2/3] chore: remove buffer from new tests --- src/tests.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tests.js b/src/tests.js index 437c7de..db255e6 100644 --- a/src/tests.js +++ b/src/tests.js @@ -355,7 +355,7 @@ module.exports = (test) => { })) it('allows mutating the datastore during a query', async () => { - const hello3 = { key: new Key('/z/4hello3'), value: Buffer.from('4') } + 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 @@ -380,7 +380,7 @@ module.exports = (test) => { }) it('queries while the datastore is being mutated', async () => { - const writePromise = store.put(new Key(`/z/key-${Math.random()}`), Buffer.from('0')) + const writePromise = store.put(new Key(`/z/key-${Math.random()}`), utf8Encoder.encode('0')) const results = await all(store.query({})) expect(results.length).to.be.greaterThan(0) await writePromise From 1949441d35345ac4d2b2b2c61eaa5232afd43ded Mon Sep 17 00:00:00 2001 From: achingbrain Date: Wed, 29 Jul 2020 11:13:33 +0100 Subject: [PATCH 3/3] chore: address pr comments --- src/key.js | 9 ++++++--- test/key.spec.js | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/src/key.js b/src/key.js index 94dac39..759e7ed 100644 --- a/src/key.js +++ b/src/key.js @@ -67,7 +67,7 @@ class Key { * * @returns {Uint8Array} */ - toBuffer () { + uint8Array () { return this._buf } @@ -118,12 +118,15 @@ class Key { } if (this._buf[0] !== pathSep) { - this._buf = Uint8Array.of(pathSep, ...this._buf) + const bytes = new Uint8Array(this._buf.byteLength + 1) + bytes.fill(pathSep, 0, 1) + bytes.set(this._buf, 1) + this._buf = bytes } // normalize does not remove trailing slashes while (this._buf.byteLength > 1 && this._buf[this._buf.byteLength - 1] === pathSep) { - this._buf = Uint8Array.of(...this._buf.slice(0, -1)) + this._buf = this._buf.subarray(0, -1) } } diff --git a/test/key.spec.js b/test/key.spec.js index 6b7c87a..2bd2c89 100644 --- a/test/key.spec.js +++ b/test/key.spec.js @@ -156,4 +156,44 @@ describe('Key', () => { // but has the same value expect(originalKey.concat().toString()).to.equal('/a/b/c') }) + + it('uint8Array', () => { + const arr = Uint8Array.from(['/'.charCodeAt(0), 0, 1, 2, 3]) + const key = new Key(arr) + const buf = key.uint8Array() + + expect(buf).to.deep.equal(arr) + }) + + it('uint8Array with surplus bytes', () => { + const arr = Uint8Array.from(['/'.charCodeAt(0), 0, 1, 2, 3, 4]) + const view = new Uint8Array(arr.buffer, 0, arr.length - 1) + + // should be same buffer + expect(view.buffer).to.equal(arr.buffer) + expect(view.buffer.byteLength).to.equal(arr.buffer.byteLength) + + // view should be shorter than wrapped buffer + expect(view.length).to.be.lessThan(arr.buffer.byteLength) + expect(view.byteLength).to.be.lessThan(arr.buffer.byteLength) + + const key = new Key(view) + const buf = key.uint8Array() + + expect(buf).to.deep.equal(view) + }) + + it('uint8Array with trailing slashes', () => { + const slash = '/'.charCodeAt(0) + const arrWithSlashes = Uint8Array.from([slash, 0, 1, 2, 3, slash, slash, slash]) + const arrWithoutSlashes = Uint8Array.from([slash, 0, 1, 2, 3]) + const key = new Key(arrWithSlashes) + const buf = key.uint8Array() + + // slashes should have been stripped + expect(buf).to.deep.equal(arrWithoutSlashes) + + // should be a view on the original buffer + expect(buf.buffer).to.equal(arrWithSlashes.buffer) + }) })