From d2c73443758103c5dbeeb29512ecf407d11ad7d1 Mon Sep 17 00:00:00 2001 From: Vincent Weevers Date: Sat, 17 Aug 2019 19:48:59 +0200 Subject: [PATCH 1/3] Drop support of value types other than string and Buffer --- README.md | 45 ++++++--------------------------------------- UPGRADING.md | 2 +- memdown.js | 14 ++++++++------ test.js | 23 ----------------------- 4 files changed, 15 insertions(+), 69 deletions(-) diff --git a/README.md b/README.md index 75df822..df7f17e 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ Your data is discarded when the process ends or you release a reference to the s ## Data types -Keys can be strings or Buffers. Any other key type will be irreversibly stringified. Unlike [`leveldown`] though, `memdown` does not stringify values. This means that in addition to Buffers, you can store any JS value without the need for [`encoding-down`]. The only exceptions are `null` and `undefined`. Keys and values of that type are rejected. +Keys and values can be strings or Buffers. Any other key type will be irreversibly stringified. The only exceptions are `null` and `undefined`. Keys and values of that type are rejected. ```js const db = levelup(memdown()) @@ -53,18 +53,17 @@ db.put('example', 123, (err) => { valueAsBuffer: false }).on('data', (entry) => { console.log(typeof entry.key) // 'string' - console.log(typeof entry.value) // 'number' + console.log(typeof entry.value) // 'string' }) }) ``` -If you desire normalization for values (e.g. to stringify numbers), wrap `memdown` with [`encoding-down`]. Alternatively install [`level-mem`] which conveniently bundles [`levelup`], `memdown` and [`encoding-down`]. Such an approach is also recommended if you want to achieve universal (isomorphic) behavior. For example, you could have [`leveldown`] in a backend and `memdown` in the frontend. +If you desire non-destructive encoding (e.g. to store and retrieve numbers as-is), wrap `memdown` with [`encoding-down`]. Alternatively install [`level-mem`] which conveniently bundles [`levelup`], `memdown` and [`encoding-down`]. Such an approach is also recommended if you want to achieve universal (isomorphic) behavior. For example, you could have [`leveldown`] in a backend and `memdown` in the frontend. ```js const encode = require('encoding-down') -const db = levelup(encode(memdown())) +const db = levelup(encode(memdown(), { valueEncoding: 'json' })) -// The default value encoding is utf8, which stringifies input. db.put('example', 123, (err) => { if (err) throw err @@ -73,46 +72,14 @@ db.put('example', 123, (err) => { valueAsBuffer: false }).on('data', (entry) => { console.log(typeof entry.key) // 'string' - console.log(typeof entry.value) // 'string' + console.log(typeof entry.value) // 'number' }) }) ``` ## Snapshot guarantees -A `memdown` store is backed by [a fully persistent data structure](https://www.npmjs.com/package/functional-red-black-tree) and thus has snapshot guarantees. Meaning that reads operate on a snapshot in time, unaffected by simultaneous writes. Do note `memdown` cannot uphold this guarantee for (copies of) object references. If you store object values, be mindful of mutating referenced objects: - -```js -const db = levelup(memdown()) -const obj = { thing: 'original' } - -db.put('key', obj, (err) => { - obj.thing = 'modified' - - db.get('key', { asBuffer: false }, (err, value) => { - console.log(value === obj) // true - console.log(value.thing) // 'modified' - }) -}) -``` - -Conversely, when `memdown` is wrapped with [`encoding-down`] it stores representations rather than references. - -```js -const encode = require('encoding-down') - -const db = levelup(encode(memdown(), { valueEncoding: 'json' })) -const obj = { thing: 'original' } - -db.put('key', obj, (err) => { - obj.thing = 'modified' - - db.get('key', { asBuffer: false }, (err, value) => { - console.log(value === obj) // false - console.log(value.thing) // 'original' - }) -}) -``` +A `memdown` store is backed by [a fully persistent data structure](https://www.npmjs.com/package/functional-red-black-tree) and thus has snapshot guarantees. Meaning that reads operate on a snapshot in time, unaffected by simultaneous writes. ## Test diff --git a/UPGRADING.md b/UPGRADING.md index 1ce4ed6..6664a64 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -4,7 +4,7 @@ This document describes breaking changes and how to upgrade. For a complete list ## v5 (unreleased) -Support of keys other than strings and Buffers has been dropped. Internally `memdown` now stores keys as Buffers which solves a number of compatibility issues ([#186](https://github.com/Level/memdown/issues/186)). If you pass in a key that isn't a string or Buffer, it will be irreversibly stringified. +Support of keys & values other than strings and Buffers has been dropped. Internally `memdown` now stores keys & values as Buffers which solves a number of compatibility issues ([#186](https://github.com/Level/memdown/issues/186)). If you pass in a key or value that isn't a string or Buffer, it will be irreversibly stringified. ## v4 diff --git a/memdown.js b/memdown.js index 10cbbf0..5bfb567 100644 --- a/memdown.js +++ b/memdown.js @@ -102,8 +102,8 @@ MemIterator.prototype._next = function (callback) { key = key.toString() } - if (this.valueAsBuffer && !Buffer.isBuffer(value)) { - value = Buffer.from(String(value)) + if (!this.valueAsBuffer) { + value = value.toString() } this._tree[this._incr]() @@ -138,7 +138,9 @@ MemIterator.prototype._outOfRange = function (target) { } MemIterator.prototype._seek = function (target) { - // TODO: conversions - i.e. string keys, with buffer target. + if (target.length === 0) { + throw new Error('cannot seek() to an empty target') + } if (this._outOfRange(target)) { this._tree = this.db._store.end @@ -172,7 +174,7 @@ MemDOWN.prototype._serializeKey = function (key) { } MemDOWN.prototype._serializeValue = function (value) { - return value + return Buffer.isBuffer(value) ? value : Buffer.from(String(value)) } MemDOWN.prototype._put = function (key, value, options, callback) { @@ -197,8 +199,8 @@ MemDOWN.prototype._get = function (key, options, callback) { }) } - if (options.asBuffer !== false && !Buffer.isBuffer(value)) { - value = Buffer.from(String(value)) + if (!options.asBuffer) { + value = value.toString() } setImmediate(function callNext () { diff --git a/test.js b/test.js index 56ca491..34d9c91 100644 --- a/test.js +++ b/test.js @@ -2,18 +2,10 @@ var test = require('tape') var suite = require('abstract-leveldown/test') var concat = require('level-concat-iterator') var memdown = require('.').default -var MemIterator = require('.').MemIterator var ltgt = require('ltgt') var Buffer = require('safe-buffer').Buffer var noop = function () { } -// Temporary fix for abstract seek tests, which currently assume -// that buffers are stored the same as strings (i.e. as byte arrays). -var baseSeek = MemIterator.prototype._seek -MemIterator.prototype._seek = function (target) { - return baseSeek.call(this, String(target)) -} - var testCommon = suite.common({ test: test, factory: function () { @@ -455,21 +447,6 @@ test('number keys', function (t) { }) }) -test('object value', function (t) { - t.plan(2) - - var db = testCommon.factory() - var obj = {} - - db.open(noop) - db.put('key', obj, noop) - - db.get('key', { asBuffer: false }, function (err, value) { - t.ifError(err, 'no get error') - t.ok(value === obj, 'same object') - }) -}) - function stringBuffer (value) { return Buffer.from(String(value)) } From b6c298c61fb1dc54ae380f467e4a5895a4ff1a7d Mon Sep 17 00:00:00 2001 From: Vincent Weevers Date: Sat, 17 Aug 2019 19:54:18 +0200 Subject: [PATCH 2/3] Test mixed use of string/buffer on values too --- test.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/test.js b/test.js index 34d9c91..70255a6 100644 --- a/test.js +++ b/test.js @@ -356,7 +356,7 @@ test('put multiple times', function (t) { }) }) -test('put key as string, get as buffer and vice versa', function (t) { +test('put as string, get as buffer and vice versa', function (t) { t.plan(7) var db = testCommon.factory() @@ -367,13 +367,13 @@ test('put key as string, get as buffer and vice versa', function (t) { db.put('a', 'a', function (err) { t.ifError(err, 'no put error') - db.get(Buffer.from('a'), { asBuffer: false }, function (err, value) { + db.get(Buffer.from('a'), { asBuffer: true }, function (err, value) { t.ifError(err, 'no get error') - t.is(value, 'a', 'got value') + t.same(value, Buffer.from('a'), 'got value') }) }) - db.put(Buffer.from('b'), 'b', function (err) { + db.put(Buffer.from('b'), Buffer.from('b'), function (err) { t.ifError(err, 'no put error') db.get('b', { asBuffer: false }, function (err, value) { @@ -384,7 +384,7 @@ test('put key as string, get as buffer and vice versa', function (t) { }) }) -test('put key as string, iterate as buffer', function (t) { +test('put as string, iterate as buffer', function (t) { t.plan(4) var db = testCommon.factory() @@ -395,9 +395,9 @@ test('put key as string, iterate as buffer', function (t) { db.put('a', 'a', function (err) { t.ifError(err, 'no put error') - concat(db.iterator({ keyAsBuffer: true, valueAsBuffer: false }), function (err, entries) { + concat(db.iterator({ keyAsBuffer: true, valueAsBuffer: true }), function (err, entries) { t.ifError(err, 'no concat error') - t.same(entries, [{ key: Buffer.from('a'), value: 'a' }]) + t.same(entries, [{ key: Buffer.from('a'), value: Buffer.from('a') }]) }) }) }) @@ -411,7 +411,7 @@ test('put key as buffer, iterate as string', function (t) { db.open(function (err) { t.ifError(err, 'no error from open') - db.put(Buffer.from('a'), 'a', function (err) { + db.put(Buffer.from('a'), Buffer.from('a'), function (err) { t.ifError(err, 'no put error') concat(db.iterator({ keyAsBuffer: false, valueAsBuffer: false }), function (err, entries) { From ca4ef70ed6342ff0a8d16664be9f1cba5db6cee1 Mon Sep 17 00:00:00 2001 From: Vincent Weevers Date: Sat, 17 Aug 2019 19:57:52 +0200 Subject: [PATCH 3/3] Rename test (where my bikeshed at) --- test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test.js b/test.js index 70255a6..e549edb 100644 --- a/test.js +++ b/test.js @@ -403,7 +403,7 @@ test('put as string, iterate as buffer', function (t) { }) }) -test('put key as buffer, iterate as string', function (t) { +test('put as buffer, iterate as string', function (t) { t.plan(4) var db = testCommon.factory()